Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- A list larger than the maximum scroll size of the browser shows its last items, and its items move evenly during a scroll. [#2410](https://github.com/IgniteUI/igniteui-webcomponents/pull/2410)
- A new `data` array measures the new items at the rendered indices again. [#2412](https://github.com/IgniteUI/igniteui-webcomponents/pull/2412)
- A `data` change in the same task as `scrollToIndex()` renders at the new offset. [#2545](https://github.com/IgniteUI/igniteui-webcomponents/pull/2545)
- A property change on a detached virtual scroll no longer throws an error.

### Security
- #### Tile manager
Expand Down
5 changes: 1 addition & 4 deletions src/components/button/button-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,7 @@ export abstract class IgcButtonBaseComponent extends EventEmitterMixin<IgcButton
this._commandfor = value;
if (value) {
this._resolver.observe();
// detached element has no root to call `getElementById` on until `connectedCallback`
this._commandForElement = this.isConnected
? getElementByIdFromRoot(this, value)
: null;
this._commandForElement = getElementByIdFromRoot(this, value);
} else {
this._commandForElement = null;
this._resolver.unobserve();
Expand Down
17 changes: 2 additions & 15 deletions src/components/highlight/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,6 @@ import type IgcHighlightComponent from './highlight.js';

type Match = { node: Node; indices: [start: number, end: number] };

/**
* Resolves the tree scope that must own the `::highlight()` rules. The rules are
* tree-scoped, so slotted content needs the host root node, not the render root.
* Firefox enforces this, Chromium currently does not.
*
* Returns `null` while the host is detached or inside a non-styleable fragment.
*/
function getStyleRoot(
host: IgcHighlightComponent
): (DocumentOrShadowRoot & Node) | null {
const root = getRoot(host);
return 'adoptedStyleSheets' in root ? root : null;
}

/* jsonAPIPlainObject */
/* marshalByValue */
/**
Expand Down Expand Up @@ -271,7 +257,8 @@ class HighlightService implements ReactiveController {
return;
}

const root = getStyleRoot(this._host);
// `::highlight()` rules are tree-scoped (Firefox enforces it): slotted content needs the host's root.
const root = getRoot(this._host);

if (this._attachedRoot !== root) {
this._removeStyleSheet();
Expand Down
4 changes: 2 additions & 2 deletions src/components/popover/position/native.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isServer } from 'lit';
import { getRoot, isPopoverOpen } from '#internals/utils/dom.js';
import { isPopoverOpen } from '#internals/utils/dom.js';
import { toggleEventListener } from '#internals/utils/events.js';
import {
applyArrowStyles,
Expand Down Expand Up @@ -106,7 +106,7 @@ export class NativePositionStrategy extends PopoverPositionStrategy {
super.attach(target, container);

container.toggleAttribute('data-anchored', true);
this._anchorObserver.observe(getRoot(target), {
this._anchorObserver.observe(target.getRootNode(), {
childList: true,
subtree: true,
});
Expand Down
3 changes: 1 addition & 2 deletions src/components/radio/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
createGroupRegistry,
type GroupMemberController,
} from '#internals/controllers/group.js';
import { getRoot } from '#internals/utils/dom.js';
import type IgcRadioComponent from './radio.js';

/**
Expand All @@ -14,7 +13,7 @@ import type IgcRadioComponent from './radio.js';
*/
const radioGroups = createGroupRegistry<IgcRadioComponent, boolean>({
keyOf: (radio) => radio.name || '',
scopeOf: (radio) => radio.form ?? getRoot(radio),
scopeOf: (radio) => radio.form ?? radio.getRootNode(),
deriveState: (radios) =>
radios.some((radio) => radio.checked && !radio.disabled),
});
Expand Down
2 changes: 1 addition & 1 deletion src/components/stepper/stepper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ export default class IgcStepperComponent extends EventEmitterMixin<IgcStepperCom

private _getActiveStepComponent(): IgcStepComponent | null {
return (
getRoot(this).activeElement?.closest(IgcStepComponent.tagName) ?? null
getRoot(this)?.activeElement?.closest(IgcStepComponent.tagName) ?? null
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/components/virtualization/recycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ function focusedPosition(
parts: readonly ChildPart[]
): number {
const container = containerPart.parentNode;
let node: Node | null = getRoot(container).activeElement;
let node: Node | null = getRoot(container)?.activeElement ?? null;

while (node && node.parentNode !== container) {
node = node.parentNode;
Expand Down
4 changes: 3 additions & 1 deletion src/components/virtualization/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
| 8 | 2026-10-21 | API descriptions aligned with the JSDoc. |
| 8 | 2026-10-05 | The stylesheet comes back after other code replaces the sheets of the tree scope |
| 9 | 2026-11-11 | `scrollToIndex` renders the new window in the same task; a hidden list keeps its window |
| 10 | 2026-11-12 | A detached list updates without adopting the stylesheet, and adopts it once attached |

## Overview

Expand Down Expand Up @@ -351,7 +352,8 @@ scope, the component adopts its stylesheet again on its next update.
### Rendering

4. Nothing is rendered without an `itemTemplate`, and the track and the content elements are rendered with one.
5. The stylesheet is adopted again on the next update after other code replaces the sheets of the tree scope.
5. The stylesheet is adopted again on the next update after other code replaces the sheets of the tree scope. A
detached list updates and keeps its styles once attached again.

### Events tests

Expand Down
19 changes: 19 additions & 0 deletions src/components/virtualization/virtualization.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,25 @@ describe('VirtualScroll', () => {
document.adoptedStyleSheets = sheets;
}
});

it('updates while detached and keeps its styles once attached again', async () => {
const el = await fixture<IgcVirtualScrollComponent<string>>(
html`<igc-virtual-scroll
.data=${createItems(100)}
.itemTemplate=${fixedTemplate}
></igc-virtual-scroll>`
);
const parent = el.parentElement!;

el.remove();
el.keyFunction = (item) => item;
await elementUpdated(el);

parent.append(el);
await elementUpdated(el);

expect(getComputedStyle(el).overflowY).to.equal('auto');
});
});

describe('Events', () => {
Expand Down
5 changes: 5 additions & 0 deletions src/components/virtualization/virtualization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,11 @@ export default class IgcVirtualScrollComponent<
}

const root = getRoot(this);
// A detached scroller adopts the sheet in `connectedCallback`.
if (!root) {
return;
}

const sheet = IgcVirtualScrollComponent._getStyleSheet();
if (!root.adoptedStyleSheets.includes(sheet)) {
root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
Expand Down
4 changes: 2 additions & 2 deletions src/internals/bases/alert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { createMutationController } from '../controllers/mutation-observer.js';
import { addToggleController } from '../controllers/toggle.js';
import { coercedProperty } from '../decorators/coerced-property.js';
import { createTimer } from '../timing.js';
import { getRoot, getVisibleAncestor, isPopoverOpen } from '../utils/dom.js';
import { getVisibleAncestor, isPopoverOpen } from '../utils/dom.js';
import { getEventOrigin } from '../utils/events.js';
import { nanoid } from '../utils/strings.js';

Expand Down Expand Up @@ -278,7 +278,7 @@ export abstract class IgcBaseAlertLikeComponent extends LitElement {
// anchor names of its own. Otherwise the container is the source of the
// popover, and the browser moves the popover after it in the tab order.
if (
getRoot(container) === getRoot(this) &&
container.getRootNode() === this.getRootNode() &&
addAnchorName(container, this._anchorName)
) {
this._anchor = container;
Expand Down
3 changes: 1 addition & 2 deletions src/internals/controllers/group.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { ReactiveController, ReactiveControllerHost } from 'lit';
import { isEmpty } from '../utils/arrays.js';
import { getRoot } from '../utils/dom.js';
import { getOrInsertComputed } from '../utils/objects.js';

type GroupHost = ReactiveControllerHost & Element;
Expand Down Expand Up @@ -71,7 +70,7 @@ export function createGroupRegistry<T extends GroupHost, S>(
const controllers = new WeakMap<T, Member>();

function scopeOf(host: T): GroupScope {
return config.scopeOf?.(host) ?? getRoot(host);
return config.scopeOf?.(host) ?? host.getRootNode();
}

/** Derives the group state and gives it to each member. */
Expand Down
2 changes: 1 addition & 1 deletion src/internals/controllers/roving-focus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ class RovingFocusController<T extends Element> {
return this._options.current();
}

const active = getRoot(this._host).activeElement;
const active = getRoot(this._host)?.activeElement;
return active
? items.find((item) => item === active || item.contains(active))
: undefined;
Expand Down
2 changes: 1 addition & 1 deletion src/internals/directives/drag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ class DraggableDirective extends PointerOperationDirective<
operation.element = stays
? previous
: (getRoot(operation.target)
.elementsFromPoint(clientX, clientY)
?.elementsFromPoint(clientX, clientY)
.find(matchTarget) ?? null);

// The first move over a match, also straight from another match, only records it.
Expand Down
14 changes: 8 additions & 6 deletions src/internals/utils/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,14 +149,16 @@ export function closestComposed(
return null;
}

/** Returns the root node (document or shadow root) of the given node. */
export function getRoot(node: Node): Document | ShadowRoot {
return node.getRootNode() as Document | ShadowRoot;
/** Returns the document or shadow root of the node, or `null` while it is disconnected. */
export function getRoot(node: Node): Document | ShadowRoot | null {
return node.isConnected
? (node.getRootNode() as Document | ShadowRoot)
: null;
}

/** Returns the element with the given id in the root node of `root`. */
export function getElementByIdFromRoot(root: HTMLElement, id: string) {
return getRoot(root).getElementById(id);
return getRoot(root)?.getElementById(id) ?? null;
}

export function isElement(node: unknown): node is Element {
Expand Down Expand Up @@ -266,9 +268,9 @@ const TABBABLE_SELECTOR =
* the document when the focus is outside `root`.
*/
export function getDeepActiveElement(
root: Document | ShadowRoot = document
root: Document | ShadowRoot | null = document
): HTMLElement | null {
let element = root.activeElement ?? document.activeElement;
let element = root?.activeElement ?? document.activeElement;

while (element?.shadowRoot?.activeElement) {
element = element.shadowRoot.activeElement;
Expand Down
Loading