Skip to content
Open
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 packages/@react-aria/test-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/

export {triggerLongPress} from './utils';
export {TestContainer} from './testContainer';
export {installMouseEvent, installPointerEvent} from './testSetup';
export {pointerMap} from './userEventMaps';
export {User} from './user';
Expand Down
77 changes: 77 additions & 0 deletions packages/@react-aria/test-utils/src/testContainer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright 2026 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

export class TestContainer {
private document: Document;
private window: Window & typeof globalThis;

constructor(type: string) {
let doctype = document.implementation.createDocumentType(type, '', '');

let iframe = document.createElement('iframe');
document.body.append(iframe);

this.document = iframe.contentDocument as typeof this.document;
this.window = iframe.contentWindow as typeof this.window;

this.document.replaceChildren();
this.document.append(doctype);
}

/**
* Returns the host node of the container.
*/
public getRootNode(): HTMLIFrameElement {
return this.window.frameElement as HTMLIFrameElement;
}

/**
* Returns the document of the container.
*/
public getDocument(): Document {
return this.document;
}

/**
* Returns the window of the container.
*/
public getWindow(): Window & typeof globalThis {
return this.window;
}

/**
* Clears all nodes in the container document.
*/
public reset(): void {
let doctype = this.document.doctype;

if (doctype != null) {
let serializer = new this.window.XMLSerializer();
let serialized = serializer.serializeToString(doctype);

this.document.open();
this.document.write(serialized);
this.document.close();
} else {
this.document.open();
this.document.write('');
this.document.close();
}
}

/**
* Removes the container from its owner document.
*/
public remove(): void {
this.window.frameElement?.remove();
}
}
3 changes: 3 additions & 0 deletions packages/@react-aria/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ export {
} from 'react-aria/private/utils/shadowdom/ShadowTreeWalker';
export {
getActiveElement,
getParentElement,
getParentNode,
getPropagationTargets,
getEventTarget,
nodeContains,
isFocusWithin
Expand Down
15 changes: 2 additions & 13 deletions packages/@react-types/shared/src/collections.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
* governing permissions and limitations under the License.
*/

import {Key} from '@react-types/shared';
import {Key} from './key';
import {LinkDOMProps} from './dom';
import {ReactElement, ReactNode} from 'react';
import {Rect, Size} from './layout';

export interface ItemProps<T> extends LinkDOMProps {
/** Rendered contents of the item or child items. */
Expand Down Expand Up @@ -132,18 +133,6 @@ export interface KeyboardDelegate {
getKeyForSearch?(search: string, fromKey?: Key | null): Key | null;
}

export interface Rect {
x: number;
y: number;
width: number;
height: number;
}

export interface Size {
width: number;
height: number;
}

/** A LayoutDelegate provides layout information for collection items. */
export interface LayoutDelegate {
/** Returns a rectangle for the item with the given key. */
Expand Down
1 change: 1 addition & 0 deletions packages/@react-types/shared/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ export * from './labelable';
export * from './orientation';
export * from './locale';
export * from './key';
export * from './layout';
45 changes: 45 additions & 0 deletions packages/@react-types/shared/src/layout.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2026 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

export type BoundingNode = Element | Document;

export type Axis = 'block' | 'inline';
export type Model = 'margin-box' | 'border-box' | 'padding-box' | 'content-box';
export type Corner = 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
export type Position = 'start' | 'center' | 'end';
export type Precision = 'pixel' | 'sub-pixel' | 'device-pixel';

export interface BoundingOptions {
/** The pixel precision to calculate the bound with. */
precision?: Precision;
/** Whether or not to allow transforms on the bound. */
transform?: boolean;
/** The box-model to use when bounding. */
model?: Model;
}

export interface Point {
x: number;
y: number;
}

export interface Rect {
x: number;
y: number;
width: number;
height: number;
}

export interface Size {
width: number;
height: number;
}
4 changes: 4 additions & 0 deletions packages/dev/eslint-plugin-rsp-rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import noReactKey from './rules/no-react-key.js';
import pureRender from './rules/pure-render.js';
import safeEventTarget from './rules/safe-event-target.js';
import shadowSafeActiveElement from './rules/shadow-safe-active-element.js';
// import shadowSafeParentElement from './rules/shadow-safe-parent-element.js';
// import shadowSafeParentNode from './rules/shadow-safe-parent-node.js';
import sortImports from './rules/sort-imports.js';
import useLayoutEffectRule from './rules/use-layout-effect-rule.js';

Expand All @@ -36,6 +38,8 @@ const rules = {
'no-non-shadow-contains': noNonShadowContains,
'safe-event-target': safeEventTarget,
'shadow-safe-active-element': shadowSafeActiveElement,
// 'shadow-safe-parent-element': shadowSafeParentElement,
// 'shadow-safe-parent-node': shadowSafeParentNode,
'faster-node-contains': fasterNodeContains,
imports,
'use-layout-effect-rule': useLayoutEffectRule,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright 2026 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

const plugin = {
meta: {
type: 'suggestion',
docs: {
description:
'Disallow using node.parentElement in favor of getParentElement() for shadow DOM compatibility',
recommended: true
},
fixable: 'code',
messages: {
useGetParentElement:
'Use getParentElement() instead of node.parentElement for shadow DOM compatibility.'
}
},
create: context => {
let hasGetParentElementImport = false;
let getParentElementLocalName = 'getParentElement';
let existingReactAriaUtilsImport = null;

return {
// Track imports from @react-aria/utils
ImportDeclaration(node) {
if (
node.source &&
node.source.type === 'Literal' &&
node.source.value === '@react-aria/utils'
) {
existingReactAriaUtilsImport = node;
// Check if getParentElement is already imported
const hasGetParentElement = node.specifiers.some(
spec =>
spec.type === 'ImportSpecifier' &&
spec.imported.type === 'Identifier' &&
spec.imported.name === 'getParentElement'
);
if (hasGetParentElement) {
hasGetParentElementImport = true;
const getParentElementSpec = node.specifiers.find(
spec =>
spec.type === 'ImportSpecifier' &&
spec.imported.type === 'Identifier' &&
spec.imported.name === 'getParentElement'
);
getParentElementLocalName = getParentElementSpec.local.name;
}
}
},

// Detect node.parentElement usage
["MemberExpression[computed=false][property.name='parentElement']"](node) {
context.report({
node,
messageId: 'useGetParentElement',
fix: fixer => {
const fixes = [];
const sourceCode = context.sourceCode;

// Replace node.parentElement with getParentElement(node)
fixes.push(
fixer.replaceText(
node,
`${getParentElementLocalName}(${sourceCode.getText(node.object)})`
)
);

// Add import if not present
if (!hasGetParentElementImport) {
if (existingReactAriaUtilsImport) {
// Add getParentElement to existing @react-aria/utils import
const specifiers = existingReactAriaUtilsImport.specifiers;
if (specifiers.length > 0) {
fixes.push(
fixer.insertTextAfter(
sourceCode.getFirstToken(
existingReactAriaUtilsImport,
token => token.value === '{'
),
'getParentElement, '
)
);
}
} else {
// No existing import from @react-aria/utils, create a new one
const programNode = context.sourceCode.ast;
const imports = programNode.body.filter(node => node.type === 'ImportDeclaration');

if (imports.length > 0) {
const lastImport = imports[imports.length - 1];
const importStatement = "\nimport {getParentElement} from '@react-aria/utils';";
fixes.push(fixer.insertTextAfter(lastImport, importStatement));
} else {
// No imports, add at the beginning
const importStatement = "import {getParentElement} from '@react-aria/utils';\n";
fixes.push(fixer.insertTextBefore(programNode.body[0], importStatement));
}
}

// Mark as imported for subsequent fixes in the same file
hasGetParentElementImport = true;
}

return fixes;
}
});
}
};
}
};

export default plugin;
Loading
Loading