Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Virtualized Property Grid: display loader on all loads #660

Merged
merged 21 commits into from
Jan 16, 2024
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
10 changes: 9 additions & 1 deletion common/api/components-react.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -3567,7 +3567,7 @@ export function usePropertyGridModel(props: {
modelSource: IPropertyGridModelSource;
}): IPropertyGridModel | undefined;

// @public
// @public @deprecated
export function usePropertyGridModelSource(props: {
dataProvider: IPropertyDataProvider;
}): PropertyGridModelSource;
Expand All @@ -3590,6 +3590,14 @@ export function useToolbarWithOverflowDirectionContext(): ToolbarOverflowContext
// @internal (undocumented)
export function useToolItemEntryContext(): ToolbarItemContextArgs;

// @public
export function useTrackedPropertyGridModelSource(props: {
dataProvider: IPropertyDataProvider;
}): {
modelSource: PropertyGridModelSource;
inProgress: boolean;
};

// @public
export function useTreeEventsHandler<TEventsHandler extends TreeEventHandler>(factoryOrParams: (() => TEventsHandler) | TreeEventHandlerParams): TreeEventHandler;

Expand Down
2 changes: 2 additions & 0 deletions common/api/summary/components-react.exports.csv
Original file line number Diff line number Diff line change
Expand Up @@ -403,11 +403,13 @@ beta;UsePropertyFilterBuilderResult
public;usePropertyGridEventHandler(props:
public;usePropertyGridModel(props:
public;usePropertyGridModelSource(props:
deprecated;usePropertyGridModelSource(props:
internal;useRenderedStringValue(record: PropertyRecord, stringValueCalculator: (record: PropertyRecord) => string | Promise
public;useToolbarPopupAutoHideContext(): boolean
public;useToolbarPopupContext(): ToolbarPopupContextProps
internal;useToolbarWithOverflowDirectionContext(): ToolbarOverflowContextProps
internal;useToolItemEntryContext(): ToolbarItemContextArgs
public;useTrackedPropertyGridModelSource(props:
public;useTreeEventsHandler
public;useTreeModel(modelSource: TreeModelSource): TreeModel
public;useTreeModelSource(dataProvider: TreeDataProvider): TreeModelSource
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@itwin/components-react",
"comment": "Show loading spinner in subsequent loads if delay threshold is reached VirtualizedPropertyGrid",
"type": "patch"
}
],
"packageName": "@itwin/components-react"
}
8 changes: 8 additions & 0 deletions docs/changehistory/NextVersion.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@

Table of contents:

- [@itwin/components-react](#itwincomponents-react)
- [Improvements](#improvements)
- [@itwin/appui-react](#itwinappui-react)
- [Additions](#additions)

## @itwin/components-react

### Improvements

- Show loading spinner in subsequent loads if delay threshold is reached `VirtualizedPropertyGrid.`

## @itwin/appui-react

### Additions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@
* @module PropertyGrid
*/

import React from "react";
import { DelayedSpinner } from "../../common/DelayedSpinner";
import React, { useEffect, useState } from "react";
import {
usePropertyGridEventHandler,
usePropertyGridModel,
usePropertyGridModelSource,
useTrackedPropertyGridModelSource,
} from "../internal/PropertyGridHooks";
import type { PropertyCategoryRendererManager } from "../PropertyCategoryRendererManager";
import type { IPropertyDataProvider } from "../PropertyDataProvider";
Expand All @@ -21,6 +20,7 @@ import type {
PropertyGridContentHighlightProps,
} from "./PropertyGridCommons";
import { VirtualizedPropertyGrid } from "./VirtualizedPropertyGrid";
import { ProgressRadial } from "@itwin/itwinui-react";

/** Properties for [[VirtualizedPropertyGridWithDataProvider]] React component
* @public
Expand All @@ -47,25 +47,54 @@ export interface VirtualizedPropertyGridWithDataProviderProps
export function VirtualizedPropertyGridWithDataProvider(
props: VirtualizedPropertyGridWithDataProviderProps
) {
const modelSource = usePropertyGridModelSource({
const { modelSource, inProgress } = useTrackedPropertyGridModelSource({
dataProvider: props.dataProvider,
});

const model = usePropertyGridModel({ modelSource });
const eventHandler = usePropertyGridEventHandler({ modelSource });

if (!model) {
return (
<div className="components-virtualized-property-grid-loader">
<DelayedSpinner size="large" />
</div>
);
}

return (
<VirtualizedPropertyGrid
{...props}
model={model}
eventHandler={eventHandler}
/>
<DelayedLoaderRenderer shouldRenderLoader={inProgress || !model}>
{model && (
<VirtualizedPropertyGrid
{...props}
model={model}
eventHandler={eventHandler}
/>
)}
</DelayedLoaderRenderer>
);
}

interface DelayedLoaderRendererProps {
shouldRenderLoader: boolean;
}

function DelayedLoaderRenderer({
children,
shouldRenderLoader,
}: React.PropsWithChildren<DelayedLoaderRendererProps>) {
const [showSpinner, setShowSpinner] = useState(shouldRenderLoader);
useEffect(() => {
if (!shouldRenderLoader) {
setShowSpinner(shouldRenderLoader);
return;
}

// only set a timeout when shouldRenderLoader is set to `true`
const timeout = setTimeout(() => {
setShowSpinner(shouldRenderLoader);
}, 250);

return () => clearTimeout(timeout);
}, [shouldRenderLoader]);

return !showSpinner ? (
<>{children}</>
) : (
<div className="components-virtualized-property-grid-loader">
<ProgressRadial indeterminate size={"large"} />
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ export function usePropertyData(props: {
* Custom hook that creates a [[PropertyGridModelSource]] and subscribes it to data updates from the data provider.
* @throws if/when `IPropertyDataProvider.getData()` promise is rejected. The error is thrown in the React's render loop, so it can be caught using an error boundary.
* @public
* @deprecated in 4.9.0. Use `useTrackedPropertyGridModelSource` instead.
*/
// istanbul ignore next: 'useTrackedPropertyGridModelSource' is almost identical.
export function usePropertyGridModelSource(props: {
dataProvider: IPropertyDataProvider;
}) {
Expand All @@ -67,6 +69,31 @@ export function usePropertyGridModelSource(props: {
return modelSource;
}

/**
* Custom hook that creates a [[PropertyGridModelSource]] and subscribes it to data updates from the data provider while also providing information on data update progress.
* @throws if/when `IPropertyDataProvider.getData()` promise is rejected. The error is thrown in the React's render loop, so it can be caught using an error boundary.
* @public
*/
export function useTrackedPropertyGridModelSource(props: {
dataProvider: IPropertyDataProvider;
}) {
const { value: propertyData, inProgress } = usePropertyData(props);
const { dataProvider } = { ...props };

// Model source needs to be recreated if data provider changes
const modelSource = useMemo(
() => new PropertyGridModelSource(new MutableGridItemFactory()),
// eslint-disable-next-line react-hooks/exhaustive-deps
[dataProvider]
);

useEffect(() => {
if (propertyData) modelSource.setPropertyData(propertyData);
}, [modelSource, propertyData]);

return { modelSource, inProgress };
}

/**
* Custom hook that creates memoized version of [[PropertyGridEventHandler]] that modifies given modelSource
* @public
Expand Down
19 changes: 13 additions & 6 deletions ui/components-react/src/test/common/DelayedSpinner.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,36 @@

import * as React from "react";
import { expect } from "chai";
import sinon from "sinon";
import { render, waitFor } from "@testing-library/react";
import { DelayedSpinner } from "../../components-react/common/DelayedSpinner";
import sinon from "sinon";

describe("<DelayedSpinner />", () => {
let clock: sinon.SinonFakeTimers;

before(() => {
clock = sinon.useFakeTimers(Date.now());
});

after(() => {
clock.restore();
});

it("renders spinner without delay", () => {
const { container } = render(<DelayedSpinner delay={0} />);
const spinnerNode = container.querySelector(".iui-large");
expect(spinnerNode).to.not.be.null;
});

it("renders spinner with delay", async () => {
const clock = sinon.useFakeTimers({ now: Date.now() });
const delay = 100;
const { container } = render(<DelayedSpinner delay={delay} />);
const { container } = render(<DelayedSpinner delay={100} />);
expect(container.children.length).to.be.eq(0);
expect(container.querySelector(".iui-large")).to.be.null;

clock.tick(delay);
clock.tick(100);

await waitFor(() => expect(container.children.length).to.be.eq(1));
expect(container.querySelector(".iui-large")).to.not.be.null;
clock.restore();
});

it("renders spinner with specified size", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,24 @@ import type { PropertyRecord } from "@itwin/appui-abstract";
import { Orientation } from "@itwin/core-react";
import { ColumnResizingPropertyListPropsSupplier } from "../../../components-react/propertygrid/component/ColumnResizingPropertyListPropsSupplier";
import { PropertyList } from "../../../components-react/propertygrid/component/PropertyList";
import TestUtils, { styleMatch, userEvent } from "../../TestUtils";
import { userEvent } from "../../TestUtils";
import TestUtils, { styleMatch } from "../../TestUtils";
import { render, screen, waitFor } from "@testing-library/react";

describe("ColumnResizingPropertyListPropsSupplier", () => {
let theUserTo: ReturnType<typeof userEvent.setup>;
let clock: sinon.SinonFakeTimers;
let records: PropertyRecord[];

const throttleMs = 16;
before(async () => {
await TestUtils.initializeUiComponents();
});

before(() => {
clock = sinon.useFakeTimers(Date.now());
});

beforeEach(() => {
clock = sinon.useFakeTimers({ now: Date.now() });
theUserTo = userEvent.setup({
advanceTimers: (delay) => {
clock.tick(delay);
Expand All @@ -35,7 +38,7 @@ describe("ColumnResizingPropertyListPropsSupplier", () => {
];
});

afterEach(() => {
after(() => {
clock.restore();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,36 @@ describe("VirtualizedPropertyGridWithDataProvider", () => {
.to.be.not.null;
});

it("renders loader on subsequent selections that take longer to load", async () => {
const { container, findByText } = render(
<VirtualizedPropertyGridWithDataProvider {...defaultProps} />
);

// assert that initial property grid is loaded
await waitFor(async () => findByText("Group 1"));
await waitFor(
async () =>
expect(container.querySelector(".virtualized-grid-node")).to.be.not
.null
);

// stub getData with a method that can be manually resolved
const getDataResult = new ResolvablePromise<PropertyData>();
dataProvider.getData = async () => getDataResult;

dataProvider.onDataChanged.raiseEvent();

// do not resolve the getData promise until the loader is displayed
await waitFor(
async () =>
expect(
container.querySelector(
".components-virtualized-property-grid-loader"
)
).to.be.not.null
);
});

it("renders PropertyCategoryBlocks correctly", async () => {
const { container } = render(
<VirtualizedPropertyGridWithDataProvider {...defaultProps} />
Expand Down
Loading