diff --git a/common/changes/@itwin/imodel-browser-react/main_2026-06-16-18-43.json b/common/changes/@itwin/imodel-browser-react/main_2026-06-16-18-43.json new file mode 100644 index 00000000..f814be76 --- /dev/null +++ b/common/changes/@itwin/imodel-browser-react/main_2026-06-16-18-43.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/imodel-browser-react", + "comment": "Prevent Stratakit grid-level props from being passed to cards", + "type": "patch" + } + ], + "packageName": "@itwin/imodel-browser-react" +} \ No newline at end of file diff --git a/packages/modules/imodel-browser/src/mui/containers/ITwinGrid/ITwinGridMUI.tsx b/packages/modules/imodel-browser/src/mui/containers/ITwinGrid/ITwinGridMUI.tsx index 89044dea..bce76405 100644 --- a/packages/modules/imodel-browser/src/mui/containers/ITwinGrid/ITwinGridMUI.tsx +++ b/packages/modules/imodel-browser/src/mui/containers/ITwinGrid/ITwinGridMUI.tsx @@ -22,6 +22,7 @@ import { import { BaseCardLoading } from "../../components/baseCard/BaseCardLoading"; import { NoResultsMUI } from "../../components/noResults/NoResultsMUI"; import { type ITwinTableOverridesMUI } from "../../types"; +import { stripNonTileProps } from "../../utils/stripNonTileProps"; import { type ITwinTableMUIStrings, ITwinTableMUI } from "./ITwinTableMUI"; import { type ITwinTilePropsMUI, ITwinTileMUI } from "./ITwinTileMUI"; @@ -296,7 +297,7 @@ type ITwinHookedTileProps = ITwinTilePropsMUI & { }; const noOp = () => ({} as Partial); const ITwinHookedTile = (props: ITwinHookedTileProps) => { - const { useTileState = noOp, ...iTwinTileProps } = props; + const { useTileState = noOp, ...rest } = props; const hookIdentity = React.useRef(useTileState); @@ -306,8 +307,7 @@ const ITwinHookedTile = (props: ITwinHookedTileProps) => { ); } - const tileState = useTileState(props.iTwin, iTwinTileProps); - // gridProps aren't used by ITwinTileMUI but are passed to useIndividualState - const { gridProps, ...tileProps } = props; + const tileProps = stripNonTileProps(rest); + const tileState = stripNonTileProps(useTileState(props.iTwin, rest)); return ; }; diff --git a/packages/modules/imodel-browser/src/mui/containers/iModelGrid/IModelGridMUI.tsx b/packages/modules/imodel-browser/src/mui/containers/iModelGrid/IModelGridMUI.tsx index f174dc1c..b813b43c 100644 --- a/packages/modules/imodel-browser/src/mui/containers/iModelGrid/IModelGridMUI.tsx +++ b/packages/modules/imodel-browser/src/mui/containers/iModelGrid/IModelGridMUI.tsx @@ -32,6 +32,7 @@ import { import { BaseCardLoading } from "../../components/baseCard/BaseCardLoading"; import { NoResultsMUI as NoResults } from "../../components/noResults/NoResultsMUI"; import { type IModelTableOverridesMUI } from "../../types"; +import { stripNonTileProps } from "../../utils/stripNonTileProps"; import { type IModelTileMUIProps, IModelTileMUI, @@ -453,7 +454,7 @@ type IModelHookedTileProps = IModelTileMUIProps & { const noOp = () => ({} as Partial); const IModelHookedTile = (props: IModelHookedTileProps) => { - const { useTileState = noOp, ...iModelTileProps } = props; + const { useTileState = noOp, ...rest } = props; const hookIdentity = React.useRef(useTileState); @@ -463,9 +464,9 @@ const IModelHookedTile = (props: IModelHookedTileProps) => { ); } - const tileState = useTileState(props.iModel, iModelTileProps); - - return ; + const tileProps = stripNonTileProps(rest); + const tileState = stripNonTileProps(useTileState(props.iModel, rest)); + return ; }; function removeFromRecentsAction( diff --git a/packages/modules/imodel-browser/src/mui/utils/stripNonTileProps.test.ts b/packages/modules/imodel-browser/src/mui/utils/stripNonTileProps.test.ts new file mode 100644 index 00000000..1e78833a --- /dev/null +++ b/packages/modules/imodel-browser/src/mui/utils/stripNonTileProps.test.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ +import { stripNonTileProps } from "./stripNonTileProps"; + +describe("stripNonTileProps", () => { + it("removes gridProps, useTileState, and tileProps while preserving everything else", () => { + const input = { + id: "1", + title: "Tile", + onClick: () => undefined, + gridProps: { foo: "bar" }, + useTileState: () => ({}), + tileProps: { className: "x" }, + }; + + const result = stripNonTileProps(input); + + expect(result).toEqual({ + id: "1", + title: "Tile", + onClick: input.onClick, + }); + expect(result).not.toHaveProperty("gridProps"); + expect(result).not.toHaveProperty("useTileState"); + expect(result).not.toHaveProperty("tileProps"); + }); + + it("returns a new object without mutating the source", () => { + const input = { keep: 1, gridProps: { foo: "bar" } }; + + const result = stripNonTileProps(input); + + expect(result).not.toBe(input); + expect(input).toHaveProperty("gridProps"); + }); + + it("is a no-op when none of the stripped keys are present", () => { + const input = { a: 1, b: "two" }; + + expect(stripNonTileProps(input)).toEqual(input); + }); +}); diff --git a/packages/modules/imodel-browser/src/mui/utils/stripNonTileProps.ts b/packages/modules/imodel-browser/src/mui/utils/stripNonTileProps.ts new file mode 100644 index 00000000..623fcf85 --- /dev/null +++ b/packages/modules/imodel-browser/src/mui/utils/stripNonTileProps.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +/** + * Strip props that may be passed to useIndividualState hooks but should not be + * forwarded to a tile/card component. + */ +export const stripNonTileProps = ( + source: T +): Omit => { + const { + gridProps: _gridProps, + useTileState: _useTileState, + tileProps: _tileProps, + ...stripped + } = source as T & { + gridProps?: unknown; + useTileState?: unknown; + tileProps?: unknown; + }; + return stripped; +};