PrimeVue 5.0.3 is out. Most of it is Select, which was the first component through the audit that began last month, and a fair number of the fixes close issues from the old PrimeVue repository.

npm install [email protected]

No runtime behavior is removed and no default changes. Select gets readonly, required, baseZIndex and autoZIndex, and three theming options are new: isolated mode, withShadowRoot() and classPrefix.

How the audit read Select

Each component in the audit starts from 100, and every deduction names the finding behind it. Select went through three passes, each one checking the one before it, and the score went from 59 to 48 to 46 without the code changing. The later passes measured more. The second one wrote small throwaway specs to check each suspicion instead of reading the source and trusting it, and many of the findings in this post came from those specs.

The weakest area was tests. Ten behaviors were removed from Select one at a time, with the suite run after each, and seven of the ten removals left every test green. So most fixes in this release add a test as well.

Select has not been re-scored on 5.0.3 yet. The next run will show how far this release moved it, and that number goes out here too.

Issues from the old repository

Before writing anything new, the audit went through every open Select and Dropdown issue in the old PrimeVue repository and checked each one against the current source. Some were already fixed and only needed closing. These were not, and 5.0.3 fixes them.

A value set before the options arrive. primefaces/primevue#3591 was the most serious item on the list, because it breaks the usual remote data pattern: the model is set, the options load later, and in between the Select shows the placeholder as if it were empty. The label was only ever looked up in the option list. When the model holds the option object itself, the label now comes from the value, so the field shows what it holds before the options load. With optionValue set the model is only a key, and there is nothing to read a label from until the options arrive, so that case still shows the placeholder.

Typing on a phone. In editable mode, typing did not work at all on Android (primefaces/primevue#5156). The cause was a separate keyboard path behind a user agent check that called preventDefault() on every key except Backspace and Enter. The same branch let Enter fall through into the main handler, so it opened and immediately closed the list, and arrows, Escape and Tab did nothing. That branch is gone, and Android now goes through the same keyboard handling as every other platform.

required. It was not a prop, so the attribute fell through to the root element and did nothing (primefaces/primevue#2943). It is a prop now, and so is readonly. A readonly Select stays focusable but does not open and does not change value through any input, and AutoComplete got the same readonly prop:

<Select v-model="city" :options="cities" optionLabel="name" required />
<Select v-model="city" :options="cities" optionLabel="name" readonly />

z-index. primefaces/primevue#4300: the overlay took its z-index from the global configuration only, so a Select inside a container with its own stacking could not be raised above it. baseZIndex and autoZIndex work per instance now, as they do on other overlay components:

<Select v-model="city" :options="cities" optionLabel="name" :baseZIndex="2000" />

Three more from the same list:

  • With filter and editable both enabled, typing went into the filter instead of the input (primefaces/primevue#8380).
  • Picking an option with the mouse could select text elsewhere on the page (primefaces/primevue#8409).
  • In the pass-through options of option rows, context was undefined, so a pt function could not style a row by its state (primefaces/primevue#8606).

Pointer and keyboard

The audit also found problems in input paths that had no issue yet.

  • Right and middle mouse buttons selected an option. Only the primary button does now.
  • A disabled option could be selected with the mouse. The only guard was a CSS rule setting pointer-events: none, and in unstyled mode there is no CSS, so there was no guard. The check is in the handler now.
  • Picking an option and reopening the list straight away left it closed, because hide() ran on a timer that show() could not cancel.
  • The blur handler also ran on a timer and did not check whether focus had come back, so right after a mouse selection the component dropped its focus state while the trigger still had focus.
  • With a filter enabled, Tab moved between the trigger and the filter and nowhere else, and Shift+Tab on the trigger went forward. A button in the footer could not be reached from the keyboard.
  • Enter used to confirm an IME composition in the filter closed the overlay.
  • clearCallback from the clearicon slot emptied the value while the Select was disabled or loading.
  • Filtering threw when a group had no children, and with virtual scrolling it threw while the overlay was closed.
  • Timers kept running after the component was unmounted.

Screen readers

  • The filter input had no accessible name. It is named now, and so are the filter inputs of Listbox and Tree, which had the same gap.
  • Option rows set aria-label to the plain label, and aria-label wins over content in the accessible name. A custom option template with a flag or a second line was announced as the bare label. That is fixed in Select, Listbox, CascadeSelect and AutoComplete.
  • The empty message and group headers carried role="option" and were read out as options that could not be chosen.
  • aria-controls pointed at an element that did not exist while the overlay was closed.
  • The editable input did not render the aria-autocomplete the documentation described.
  • The hidden focus elements around the overlay were aria-hidden and focusable at the same time.
  • A <label for> click listener stayed on the old label when inputId or labelId changed, or when the label was hidden.

Types

A few fixes only change the type definitions, and one of them can surface as a new TypeScript error:

  • SelectProps repeated the base props instead of extending them, and in the copy size and variant accepted any string. They are narrowed to the values the component handles, so an invalid value that compiled before is now an error.
  • dataKey and filterFields accept the function form that the runtime already accepted.
  • The content slot is removed from the types. It was declared but never rendered.
  • tabindex passed as a string no longer triggers a prop type warning.

The documentation now also states that dataKey is ignored when optionValue is set. That was already how the component behaved.

Long lists

Filtering grouped options checked every option against the filtered list with includes, which is quadratic: in an isolated measurement it took 42.5 ms at 20,000 options and 1.4 ms with a Set, which is what it uses now.

The other cost was the selection check. Each option row evaluated it about six times per render, falling back to a deep comparison when there was no dataKey, and now evaluates it once.

A component that stayed unstyled for the session

An InputNumber sometimes came out unstyled and then stayed that way on every route until a full reload (discussion #4835). A component created and thrown away before it mounted marked its style as loaded without writing it, so no later instance wrote it either. In 5.0.3 the style is written when the component mounts or when it is discarded, whichever comes first. Code that loads a missing style by hand to get around this can be removed. The full explanation is in Why a PrimeVue component stays unstyled until a reload.

Isolated mode, shadow roots and classPrefix

All three are built on @primeuix/styled 1.1.0 and @primeuix/utils 0.8.3, which 5.0.3 requires, and the PrimeUIX styled 1.1.0 post covers that layer. Each one also has a longer post of its own.

Isolated mode is for a page with more than one app on it: micro frontends, or a browser extension that injects its own PrimeVue panel. In the default mode the apps share one theme and the last one to load wins. withIsolation gives an app a theme of its own, scoped to a data-p-theme attribute on its root and its overlays:

import { createApp } from 'vue';
import PrimeVue from 'primevue/config';
import { withIsolation } from '@primevue/core/isolation';
import Aura from '@primeuix/themes/aura';

const app = createApp(App);

app.use(PrimeVue, {
    theme: {
        preset: Aura
    },
    isolation: withIsolation({ key: 'shop' })
});

It is experimental, and the API may still change. Keys, CSS layers and server rendering are covered in Running two PrimeUI apps on one page.

Shadow roots. Until now a PrimeVue component inside a shadow root rendered unstyled (primefaces/primevue#4001). An app with one theme can now pass withShadowRoot(), which copies the page styles into the shadow roots that components render in. Clicks and focus inside a shadow root are also read correctly now: a click in a MultiSelect or Popover panel no longer counts as outside (primefaces/primevue#7521). Setup, dark mode and overlays are in Using PrimeUI components inside a shadow root.

classPrefix renames the p- component classes in the CSS and in the markup:

app.use(PrimeVue, {
    theme: {
        preset: Aura,
        options: {
            classPrefix: 'my'
        }
    }
});

Renaming the p- classes with classPrefix has the details, including what keeps its name.

Each component instance now also gets a data-pc-id attribute. The old empty pcN attribute is still rendered next to it, so selectors written against it keep working. New code should use data-pc-id.

Other fixes

  • dt had no effect on ConfirmDialog.
  • Opening the CascadeSelect overlay with the keyboard opened every panel of the selected path, stacked on top of each other.
  • A readonly DatePicker still opened its overlay and showed the clear icon.

Upgrading

npm install [email protected]

Most apps need nothing else. Run the type check once, since the narrowed size and variant types can turn a value that never worked into a compile error. A readonly that was set on a Select before and silently ignored now does what it says. And end-to-end tests that read option names will now see what the option template renders rather than the plain label.

The full list is in the changelog.