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
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';
52 changes: 52 additions & 0 deletions packages/@react-types/shared/src/layout.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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 Precision = 'pixel' | 'sub-pixel' | 'device-pixel';
export type Corner = 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
export type Position = 'start' | 'center' | 'end';

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

export type BoxModel =
| 'margin-box'
| 'scroll-margin-box'
| 'border-box'
| 'padding-box'
| 'scroll-padding-box'
| 'content-box';

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;
122 changes: 122 additions & 0 deletions packages/dev/eslint-plugin-rsp-rules/rules/shadow-safe-parent-node.js
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.parentNode in favor of getParentNode() for shadow DOM compatibility',
recommended: true
},
fixable: 'code',
messages: {
useGetParentNode:
'Use getParentNode() instead of node.parentNode for shadow DOM compatibility.'
}
},
create: context => {
let hasGetParentNodeImport = false;
let getParentNodeLocalName = 'getParentNode';
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 getParentNode is already imported
const hasGetParentNode = node.specifiers.some(
spec =>
spec.type === 'ImportSpecifier' &&
spec.imported.type === 'Identifier' &&
spec.imported.name === 'getParentNode'
);
if (hasGetParentNode) {
hasGetParentNodeImport = true;
const getParentNodeSpec = node.specifiers.find(
spec =>
spec.type === 'ImportSpecifier' &&
spec.imported.type === 'Identifier' &&
spec.imported.name === 'getParentNode'
);
getParentNodeLocalName = getParentNodeSpec.local.name;
}
}
},

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

// Replace node.parentNode with getParentNode(node)
fixes.push(
fixer.replaceText(
node,
`${getParentNodeLocalName}(${sourceCode.getText(node.object)})`
)
);

// Add import if not present
if (!hasGetParentNodeImport) {
if (existingReactAriaUtilsImport) {
// Add getParentNode to existing @react-aria/utils import
const specifiers = existingReactAriaUtilsImport.specifiers;
if (specifiers.length > 0) {
fixes.push(
fixer.insertTextAfter(
sourceCode.getFirstToken(
existingReactAriaUtilsImport,
token => token.value === '{'
),
'getParentNode, '
)
);
}
} 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 {getParentNode} from '@react-aria/utils';";
fixes.push(fixer.insertTextAfter(lastImport, importStatement));
} else {
// No imports, add at the beginning
const importStatement = "import {getParentNode} from '@react-aria/utils';\n";
fixes.push(fixer.insertTextBefore(programNode.body[0], importStatement));
}
}

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

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

export default plugin;
Loading