PrimeNG 22.1.0 is out. It is a maintenance release on top of 22.0, so there are no new components in it, but the list of fixes is long and two of them change what you can build rather than only what works.

npm install [email protected]

The peer range on @angular/core, @angular/cdk and the rest is ^22.1.0, so Angular 22.1 or later is required. If you are still on Angular 22.0, upgrade Angular first.

Styling now reaches inside Shadow DOM

This one comes first because it removes a category of workaround rather than fixing a bug.

Component styles were injected as <style> elements in the document head. Components living inside a shadow root could not see them: shadow boundaries block document stylesheets by design, so a PrimeNG component rendered inside a web component came out unstyled. Teams shipping Angular Elements, or using ViewEncapsulation.ShadowDom on a hosting component, had to hand-copy styles across the boundary.

You do not have to call anything to get the fix. Every component extends a base that checks its own root node during construction, and when that root node is a shadow root it registers it:

// primeng/src/basecomponent/basecomponent.ts
private _shareStylesWithShadowRoot() {
    if (isPlatformServer(this.platformId)) return;

    const rootNode = this.$el?.getRootNode?.();

    if (typeof ShadowRoot === 'undefined' || !(rootNode instanceof ShadowRoot)) return;

    inject(DestroyRef).onDestroy(inject(UseStyle).addShadowRoot(rootNode));
}

Registration returns its own cleanup function, which is handed straight to DestroyRef, so a root stops being tracked when the component that registered it is destroyed.

Underneath, the UseStyle service keeps every injected style as a constructed CSSStyleSheet and pushes it into adoptedStyleSheets on each tracked root. Adoption is two-way over time: a root registered later picks up the sheets already created, and a style created later is appended to every root already tracked. Removing a style removes it from the document and from every root that adopted it. Stylesheets a root already had are left alone, so adopting does not trample a host page’s own styles.

Two details about what is adopted and what is not. Theme variables (the primitive, semantic and global custom property blocks) are deliberately not adopted. They are loaded with variables: true and stay in the document head, because custom properties inherit through a shadow boundary on their own, so copying them into each root would buy nothing. The second detail is that constructed stylesheets drop @import, and for that reason the <style> element in the head is kept alongside the constructed sheet rather than replaced by it. The head copy is the one that still applies imported CSS.

The same release teaches DOM traversal about shadow boundaries. getParents now steps from a shadow root to its host element instead of stopping at null, so overlay positioning and scroll handling can see the real ancestor chain from inside a shadow tree.

If you have been keeping a manual copy of the theme CSS to make Angular Elements look right, you can delete that copy now.

Tree drag and drop no longer breaks under a filter

Filtering a Tree does not narrow the nodes in place. It builds shallow copies ({ ...node }) of the nodes that matched and renders those, which is fine for display, but drag and drop is a structural operation and it was moving the copy. The original array was left as it was, so clearing the filter put everything back where it started and the move looked like it had been undone.

22.1.0 keeps a WeakMap from each rendered copy back to the node it came from, so a drop reaches the real value:

// Given a node from the filtered view, get the one in your data.
// Returns the node itself when it is not a copy.
const source = tree.getOriginalNode(node);

The node rows use this themselves. UITreeNode exposes originalNode and originalSubNodes getters that resolve through the map, and the drop path moves nodes in those arrays rather than in the rendered copies. onNodeDrop reports dropNode as the original node too, so a handler receives something that exists in your data.

The Tree suite covers the behaviour directly, including a case that drops a node and then clears the filter to check the move stuck:

✓ should apply a root move to the original value and keep it when the filter is cleared
✓ should refresh the filtered view after a drop
✓ should move a filtered child node out of its parent without duplication
✓ should not allow dropping a node into its own filtered descendant

There is a related timing fix. A drop’s accept callback may be invoked asynchronously, after dragend has already cleared the drag state and reset the drop point hover signals. The drag state and drop position are now captured at drop time and passed into insertNodeOnDrop as arguments, which means a deferred accept applies the drop it was called for rather than whatever state is left over by the time it runs.

The same work fixed node duplication under virtual scrolling. With virtual scroll the row index is a position in the flattened list rather than in the sibling array, so both positions are now resolved against the sibling arrays.

Select and MultiSelect

The overlay lifecycle events here are not new. Select has emitted onShow and onHide since 22.0, and MultiSelect emits onPanelShow and onPanelHide. The names differ between the two components, so a handler cannot simply be copied across:

<p-select [options]="cities" [(ngModel)]="selectedCity" (onShow)="onOverlayShow()" (onHide)="onOverlayHide()" />

<p-multiselect [options]="plans" [(ngModel)]="selectedPlans" (onPanelShow)="onOverlayShow()" (onPanelHide)="onOverlayHide()" />

What changed in 22.1.0 is the payload type. These outputs, along with Overlay’s deprecated onAnimationStart and onAnimationDone, are now typed MotionEvent & AnimationEvent, and only the MotionEvent half of that intersection exists at runtime. The AnimationEvent half is kept for backward compatibility and the doc comment says so. Code that reads event.animationName or event.elapsedTime compiles and then finds undefined at runtime, so treat those members as gone even though the type still offers them.

optionDisabled also changed on both components. It was input<string>() and is now input<string | ((item: any) => boolean)>(), so you can pass a function without casting around the type:

<p-multiselect [options]="plans" optionLabel="name" [optionDisabled]="isPlanUnavailable" [(ngModel)]="selectedPlans" />

The field name form keeps working unchanged. Both forms go through resolveFieldData, which accepted a resolver function before this release as well, so the runtime behaviour is the same and it is the type that caught up.

Select also picked up a scroll fix. In multiple mode, opening the overlay no longer scrolls the list to the selected item, which was yanking the panel away from where you were looking.

The rest of the fixes

Table filter buttons. filterButtonProps used to be an input with a default object, so passing a partial value replaced the whole thing and the fields you left out fell back to nothing. The default is now {} and a computedFilterButtonProps signal merges what you pass over the built-in defaults, so { popover: { clear: { outlined: false } } } changes only that button.

Chip and InputTags. Chip’s visible state is now a linkedSignal keyed on what the chip renders (label, icon, image) rather than a plain signal. Angular re-binds views by position, so a chip that hid itself on remove stayed hidden for whichever item slid into that slot, which is why removing several tags in InputTags left gaps behind. Chip also stopped binding false as an inline style value, which was flooding Angular 22.1 dev builds with NG0318 warnings.

Table paginator. After the data changed, totalRecords kept a stale count, because the recalculation only ran when the previous total was zero and after the first load it never was. It tracks the current value length now.

FilterService. notEquals returned false for an empty filter value, so an empty filter excluded every row instead of matching all of them. It returns true now.

InputMask. Two fixes: the constructor effect no longer calls writeValue('') and wipes the bound value on re-initialization, and pasting into a masked input now dispatches a proper input event rather than only emitting onUnmaskedChange.

Table. table.clear() now clears filter input contents as well as filter state, and table content no longer paints in front of a frozen column.

Chart. The chart types no longer break builds that run with skipLibCheck: false.

Datepicker. Falling below minDate in the time picker no longer snaps the time to 11:00.

Menu family components (ContextMenu, MegaMenu, Steps, Dock, Menubar, TieredMenu, PanelMenu) and CascadeSelect, TreeSelect, SelectButton and MeterGroup all received smaller correctness fixes. The release touched 60 of the library’s source directories across 78 commits, so the list above gives you the shape of it and not all of it.

Upgrading

If you are on 22.0.x, there are no API changes to make, so the upgrade is a version bump:

npm install [email protected]

After that there are a few things to check. If you were working around Shadow DOM styling by duplicating CSS, remove the duplicate, because adoption happens on its own now and a stale copy will fight the real stylesheet. If you built anything on top of Tree drag and drop while a filter was active, that code was probably compensating for the copy behaviour, and it can talk to the original nodes through getOriginalNode instead. And if an overlay handler reads AnimationEvent members off onShow, onHide, onPanelShow or onPanelHide, move it to the MotionEvent properties while the old typing still compiles.