Skip to content

Commit

Permalink
[Property Grid]: Add support for React 18 (#724)
Browse files Browse the repository at this point in the history
* Update hooks and components to work in React 18 StrictMode

* Run tests with React 18

* Fix lint

* Change
  • Loading branch information
saskliutas authored Jan 8, 2024
1 parent 236769b commit 11e375d
Show file tree
Hide file tree
Showing 23 changed files with 442 additions and 409 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Added React 18 support.",
"packageName": "@itwin/property-grid-react",
"email": "24278440+saskliutas@users.noreply.github.com",
"dependentChangeType": "patch"
}
23 changes: 11 additions & 12 deletions packages/itwin/property-grid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@
"@itwin/core-react": "^4.3.0",
"@itwin/presentation-components": "^4.0.0",
"@itwin/presentation-frontend": "^4.0.0",
"react": "^17.0.0",
"react-dom": "^17.0.0"
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
},
"dependencies": {
"@itwin/itwinui-icons-react": "^2.1.0",
Expand Down Expand Up @@ -89,16 +89,15 @@
"@itwin/presentation-frontend": "^4.0.0",
"@itwin/webgl-compatibility": "^4.0.0",
"@playwright/test": "^1.36.2",
"@testing-library/dom": "^8.12.0",
"@testing-library/react": "^12.0.0",
"@testing-library/react-hooks": "^7.0.2",
"@testing-library/user-event": "14.4.3",
"@testing-library/dom": "^9.3.3",
"@testing-library/react": "^14.1.2",
"@testing-library/user-event": "^14.5.2",
"@types/chai": "4.3.5",
"@types/jsdom": "^21.1.6",
"@types/mocha": "^10.0.6",
"@types/node": "^18.18.10",
"@types/react": "^17.0.19",
"@types/react-dom": "^17.0.9",
"@types/react": "^18.0.34",
"@types/react-dom": "^18.0.11",
"@types/sinon": "^17.0.2",
"@types/sinon-chai": "^3.2.12",
"@typescript-eslint/eslint-plugin": "^5.59.8",
Expand All @@ -111,14 +110,14 @@
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-unused-imports": "^2.0.0",
"global-jsdom": "~9.1.0",
"global-jsdom": "^9.2.0",
"ignore-styles": "^5.0.1",
"jsdom": "^22.1.0",
"jsdom": "^23.1.0",
"mocha": "^10.2.0",
"nyc": "15.1.0",
"raf": "^3.4.0",
"react": "^17.0.0",
"react-dom": "^17.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-redux": "^7.2.9",
"redux": "^4.1.0",
"rimraf": "^3.0.2",
Expand Down
642 changes: 322 additions & 320 deletions packages/itwin/property-grid/pnpm-lock.yaml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/

import { useCallback } from "react";
import { useEffect, useState } from "react";
import { PropertyValueFormat } from "@itwin/appui-abstract";
import {
FilteredType, FilteringPropertyDataProvider, PropertyDataChangeEvent, PropertyRecordDataFiltererBase, VirtualizedPropertyGridWithDataProvider,
} from "@itwin/components-react";
import { useDisposable } from "@itwin/core-react";

import type { PropertyRecord } from "@itwin/appui-abstract";
import type { IPropertyDataFilterer, IPropertyDataProvider, PropertyCategory, PropertyData, PropertyDataFilterResult, VirtualizedPropertyGridWithDataProviderProps } from "@itwin/components-react";
Expand All @@ -30,13 +29,19 @@ export interface FilteringPropertyGridProps extends VirtualizedPropertyGridWithD
* @internal
*/
export function FilteringPropertyGrid({ filterer, dataProvider, autoExpandChildCategories, ...props }: FilteringPropertyGridProps) {
const filteringDataProvider = useDisposable(useCallback(
() => {
const filteringProvider = new FilteringPropertyDataProvider(dataProvider, filterer);
return new AutoExpandingPropertyFilterDataProvider(filteringProvider, autoExpandChildCategories);
},
[filterer, dataProvider, autoExpandChildCategories]
));
const [filteringDataProvider, setFilteringDataProvider] = useState<AutoExpandingPropertyFilterDataProvider>();
useEffect(() => {
const filteringProvider = new FilteringPropertyDataProvider(dataProvider, filterer);
const provider = new AutoExpandingPropertyFilterDataProvider(filteringProvider, autoExpandChildCategories);
setFilteringDataProvider(provider);
return () => {
provider.dispose();
};
}, [filterer, dataProvider, autoExpandChildCategories]);

if (!filteringDataProvider) {
return null;
}

// in order to allow resize values column fully we need to override default width reserved for action buttons.
// istanbul ignore next
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export interface AncestorsNavigationControlsProps {
* @public
*/
export function AncestorsNavigationControls({ navigateUp, navigateDown, canNavigateDown, canNavigateUp }: AncestorsNavigationControlsProps) {
// istanbul ignore if
if (!canNavigateDown && !canNavigateUp) {
return null;
}
Expand Down
28 changes: 11 additions & 17 deletions packages/itwin/property-grid/src/components/PropertyGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { PropertyGridManager } from "../PropertyGridManager";
import { FilteringPropertyGrid } from "./FilteringPropertyGrid";
import { PropertyGridContent } from "./PropertyGridContent";

import type { IModelConnection } from "@itwin/core-frontend";
import type { DataProviderProps } from "../hooks/UseDataProvider";
import type { FilteringPropertyGridProps } from "./FilteringPropertyGrid";
import type { PropertyGridContentProps } from "./PropertyGridContent";
Expand All @@ -27,7 +26,16 @@ export type PropertyGridProps = Omit<PropertyGridContentProps, "dataProvider" |
* @public
*/
export function PropertyGrid({ createDataProvider, ...props }: PropertyGridProps) {
const { dataProvider, isOverLimit } = useUnifiedSelectionDataProvider({ imodel: props.imodel, createDataProvider });
const dataProvider = useDataProvider({ imodel: props.imodel, createDataProvider });
if (!dataProvider) {
return null;
}

return <UnifiedSelectionPropertyGrid {...props} dataProvider={dataProvider} />;
}

function UnifiedSelectionPropertyGrid(props: PropertyGridContentProps) {
const { isOverLimit } = usePropertyDataProviderWithUnifiedSelection({ dataProvider: props.dataProvider });

const dataRenderer = (dataRendererProps: FilteringPropertyGridProps) => {
if (isOverLimit) {
Expand All @@ -41,19 +49,5 @@ export function PropertyGrid({ createDataProvider, ...props }: PropertyGridProps
return <FilteringPropertyGrid {...dataRendererProps} />;
};

return (
<PropertyGridContent
{...props}
dataProvider={dataProvider}
dataRenderer={dataRenderer}
/>
);
return <PropertyGridContent {...props} dataRenderer={dataRenderer} />;
}

/** Custom hook that creates data provider and hooks provider into unified selection. */
function useUnifiedSelectionDataProvider(props: DataProviderProps & { imodel: IModelConnection }) {
const dataProvider = useDataProvider(props);
const { isOverLimit } = usePropertyDataProviderWithUnifiedSelection({ dataProvider });
return { dataProvider, isOverLimit };
}

Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { PropertyGridContent } from "./PropertyGridContent";
import type { IModelConnection } from "@itwin/core-frontend";
import type { InstanceKey } from "@itwin/presentation-common";
import type { PropertyGridContentProps } from "./PropertyGridContent";
import type { DataProviderProps } from "../hooks/UseDataProvider";
import type { DataProviderProps } from "../hooks/UseDataProvider";

/**
* Props for data provider used by `SingleElementPropertyGrid`.
Expand All @@ -33,20 +33,21 @@ export type SingleElementPropertyGridProps = Omit<PropertyGridContentProps, "dat
* @public
*/
export function SingleElementPropertyGrid({ instanceKey, createDataProvider, ...props }: SingleElementPropertyGridProps) {
const dataProvider = useSingleElementDataProvider({ imodel: props.imodel, instanceKey, createDataProvider });
const dataProvider = useSingleElementDataProvider({ imodel: props.imodel, instanceKey, createDataProvider });
if (!dataProvider) {
return null;
}

return (
<PropertyGridContent
{...props}
dataProvider={dataProvider}
/>
);
return <PropertyGridContent {...props} dataProvider={dataProvider} />;
}

/** Custom hook that creates data provider and setup it to load data for specific instance. */
function useSingleElementDataProvider({ instanceKey, ...props }: SingleElementDataProviderProps & { imodel: IModelConnection }) {
const dataProvider = useDataProvider(props);
useEffect(() => {
if (!dataProvider) {
return;
}
dataProvider.keys = new KeySet([instanceKey]);
}, [dataProvider, instanceKey]);
return dataProvider;
Expand Down
21 changes: 12 additions & 9 deletions packages/itwin/property-grid/src/hooks/UseDataProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/

import { useCallback } from "react";
import { useDisposable } from "@itwin/core-react";
import { useEffect, useState } from "react";
import { PresentationPropertyDataProvider } from "@itwin/presentation-components";

import type { IModelConnection } from "@itwin/core-frontend";
Expand All @@ -24,11 +23,15 @@ export interface DataProviderProps {
* @internal
*/
export function useDataProvider({ imodel, createDataProvider }: DataProviderProps & { imodel: IModelConnection }) {
return useDisposable(
useCallback(
() => createDataProvider ? createDataProvider(imodel) : new PresentationPropertyDataProvider({ imodel }),
[imodel, createDataProvider]
)
);
}
const [state, setState] = useState<IPresentationPropertyDataProvider>();

useEffect(() => {
const provider = createDataProvider ? createDataProvider(imodel) : new PresentationPropertyDataProvider({ imodel });
setState(provider);
return () => {
provider.dispose();
};
}, [imodel, createDataProvider]);

return state;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import sinon from "sinon";
import { UiFramework } from "@itwin/appui-react";
import { BeEvent } from "@itwin/core-bentley";
import { IModelApp } from "@itwin/core-frontend";
import { render, waitFor } from "@testing-library/react";
import { render, waitFor } from "./TestUtils";
import * as multiElementPropertyGrid from "../components/MultiElementPropertyGrid";
import { PropertyGridComponent } from "../PropertyGridComponent";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ import { createRef } from "react";
import sinon from "sinon";
import { StagePanelLocation, StagePanelSection, StageUsage, UiFramework, WidgetState } from "@itwin/appui-react";
import { KeySet, StandardNodeTypes } from "@itwin/presentation-common";
import { render, waitFor } from "@testing-library/react";
import * as usePropertyGridTransientStateModule from "../hooks/UsePropertyGridTransientState";
import * as propertyGridComponent from "../PropertyGridComponent";
import { PropertyGridManager } from "../PropertyGridManager";
import { PropertyGridUiItemsProvider, PropertyGridWidgetId } from "../PropertyGridUiItemsProvider";
import { stubSelectionManager } from "./TestUtils";
import { render, stubSelectionManager, waitFor } from "./TestUtils";

import type { WidgetDef } from "@itwin/appui-react";
import type { ECClassGroupingNodeKey } from "@itwin/presentation-common";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/

import { createElement, Fragment, StrictMode } from "react";
import sinon from "sinon";
import { PropertyRecord } from "@itwin/appui-abstract";
import { BeEvent } from "@itwin/core-bentley";
import { KeySet } from "@itwin/presentation-common";
import { Presentation, SelectionChangeEvent } from "@itwin/presentation-frontend";
import { render } from "@testing-library/react";
import { renderHook as renderHookRTL, render as renderRTL } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import type { PropsWithChildren, ReactElement } from "react";
import type {
RenderHookOptions, RenderHookResult, RenderOptions, RenderResult,
} from "@testing-library/react";
import type { UserEvent } from "@testing-library/user-event";
import type { PropertyDescription, PropertyValue } from "@itwin/appui-abstract";
import type { FavoritePropertiesManager, SelectionManager, SelectionScopesManager } from "@itwin/presentation-frontend";

Expand Down Expand Up @@ -83,9 +89,37 @@ export function createResolvablePromise<T>() {
};
}

export function renderWithUser(...args: Parameters<typeof render>): ReturnType<typeof render> & { user: ReturnType<typeof userEvent.setup>} {
function createWrapper(wrapper?: React.JSXElementConstructor<{ children: React.ReactElement }>, disableStrictMode?: boolean) {
// if `DISABLE_STRICT_MODE` is set do not wrap components into `StrictMode` component
const StrictModeWrapper = process.env.DISABLE_STRICT_MODE || disableStrictMode ? Fragment : StrictMode;

return wrapper
? ({ children }: PropsWithChildren<unknown>) => <StrictModeWrapper>{createElement(wrapper, undefined, children)}</StrictModeWrapper>
: StrictModeWrapper;
}

/**
* Custom render function that wraps around `render` function from `@testing-library/react` and additionally
* setup `userEvent` from `@testing-library/user-event`.
*
* It should be used when test need to do interactions with rendered components.
*/
function customRender(ui: ReactElement, options?: RenderOptions & { disableStrictMode?: boolean }): RenderResult & { user: UserEvent } {
const wrapper = createWrapper(options?.wrapper, options?.disableStrictMode);
return {
...render(...args),
...renderRTL(ui, { ...options, wrapper }),
user: userEvent.setup(),
};
}

function customRenderHook<Result, Props>(
render: (initialProps: Props) => Result,
options?: RenderHookOptions<Props> & { disableStrictMode?: boolean },
): RenderHookResult<Result, Props> {
const wrapper = createWrapper(options?.wrapper, options?.disableStrictMode);
return renderHookRTL(render, { ...options, wrapper });
}

export * from "@testing-library/react";
export { customRender as render };
export { customRenderHook as renderHook };
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
import { expect } from "chai";
import sinon from "sinon";
import { PresentationLabelsProvider } from "@itwin/presentation-components";
import { render, waitFor } from "@testing-library/react";
import userEvents from "@testing-library/user-event";
import { ElementList } from "../../components/ElementList";
import { PropertyGridManager } from "../../PropertyGridManager";
import { render, waitFor } from "../TestUtils";

import type { IModelConnection } from "@itwin/core-frontend";
import type { InstanceKey } from "@itwin/presentation-common";
Expand Down Expand Up @@ -50,7 +50,7 @@ describe("<ElementList />", () => {
await waitFor(() => getByText(expected));
}

expect(getLabelsStub).to.be.calledOnce;
expect(getLabelsStub).to.be.calledWith(instanceKeys);
});

it("loads and orders elements by labels", async () => {
Expand Down Expand Up @@ -93,7 +93,8 @@ describe("<ElementList />", () => {

// wait for first element to be rendered
await waitFor(() => getByText(expectedLabels[0]));
expect(getLabelsStub).to.be.calledTwice;
expect(getLabelsStub).to.be.calledWith(instanceKeys.slice(0, 1000));
expect(getLabelsStub).to.be.calledWith(instanceKeys.slice(1000));
});

it("invokes `onSelect` when item is clicked", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
import { expect } from "chai";
import { PropertyRecord, PropertyValueFormat } from "@itwin/appui-abstract";
import { PropertyDataChangeEvent } from "@itwin/components-react";
import { render, waitFor } from "@testing-library/react";
import { FilteringPropertyGrid, NonEmptyValuesPropertyDataFilterer, NoopPropertyDataFilterer } from "../../property-grid-react";
import { createPropertyRecord } from "../TestUtils";
import { createPropertyRecord, render, waitFor } from "../TestUtils";

import type { IPropertyDataProvider } from "@itwin/components-react";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ import sinon from "sinon";
import { PropertyRecord, PropertyValueFormat } from "@itwin/appui-abstract";
import { KeySet } from "@itwin/presentation-common";
import { PresentationLabelsProvider, PresentationPropertyDataProvider } from "@itwin/presentation-components";
import { getByRole as getByRoleRTL, render, waitFor } from "@testing-library/react";
import userEvents from "@testing-library/user-event";
import { AncestorsNavigationControls, MultiElementPropertyGrid, PropertyGridManager } from "../../property-grid-react";
import { createPropertyRecord, stubFavoriteProperties, stubPresentation, stubSelectionManager } from "../TestUtils";
import { createPropertyRecord, getByRole as getByRoleRTL, render, stubFavoriteProperties, stubPresentation, stubSelectionManager, waitFor } from "../TestUtils";

import type { IModelConnection } from "@itwin/core-frontend";
import type { InstanceKey } from "@itwin/presentation-common";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ import sinon from "sinon";
import { PropertyRecord, PropertyValueFormat } from "@itwin/appui-abstract";
import { KeySet } from "@itwin/presentation-common";
import { PresentationPropertyDataProvider } from "@itwin/presentation-components";
import { render, waitFor } from "@testing-library/react";
import { PropertyGrid, PropertyGridManager } from "../../property-grid-react";
import { createPropertyRecord, stubFavoriteProperties, stubPresentation, stubSelectionManager } from "../TestUtils";
import { createPropertyRecord, render, stubFavoriteProperties, stubPresentation, stubSelectionManager, waitFor } from "../TestUtils";

import type { IModelConnection } from "@itwin/core-frontend";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ import { expect } from "chai";
import sinon from "sinon";
import { PropertyRecord, PropertyValueFormat } from "@itwin/appui-abstract";
import { PropertyDataChangeEvent } from "@itwin/components-react";
import { waitFor } from "@testing-library/react";
import { PropertyGridContent } from "../../components/PropertyGridContent";
import { PropertyGridSettingsMenuItem, ShowHideNullValuesSettingsMenuItem } from "../../components/SettingsDropdownMenu";
import { NullValueSettingContext } from "../../hooks/UseNullValuesSetting";
import { PropertyGridManager } from "../../PropertyGridManager";
import { createPropertyRecord, renderWithUser, stubSelectionManager } from "../TestUtils";
import { createPropertyRecord, render, stubSelectionManager, waitFor } from "../TestUtils";

import type { ReactElement } from "react";
import type { PrimitiveValue } from "@itwin/appui-abstract";
Expand Down Expand Up @@ -51,7 +50,7 @@ describe("<PropertyGridContent />", () => {
} as unknown as IPresentationPropertyDataProvider;

function renderWithContext(ui: ReactElement) {
return renderWithUser(
return render(
<NullValueSettingContext>
{ui}
</NullValueSettingContext>
Expand Down
Loading

0 comments on commit 11e375d

Please sign in to comment.