Skip to content
Open
23 changes: 23 additions & 0 deletions .changeset/free-shoes-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
"@itwin/appui-react": minor
---

Added `getPanels` property to `UiItemsProvider` interface which allows panel items to be defined. Once defined the `panels` getter of `FrontstageDef` class can be used to control the visibility and behavior of provided panels.

```tsx
UiItemsManager.register({
id: "my-provider",
getPanels: () => [
{
id: "panel1",
type: "dynamic",
placement: "left",
label: "Panel 1",
content: <>Panel 1 content</>,
},
],
});

const frontstageDef = UiFramework.frontstages.activeFrontstageDef;
frontstageDef?.panels.open({ id: "panel1" });
```
13 changes: 13 additions & 0 deletions common/api/appui-react.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ import { SnapMode } from '@itwin/core-frontend';
import type { SolarDataProvider } from '@itwin/imodel-components-react';
import { StandardViewId } from '@itwin/core-frontend';
import type { Store } from 'redux';
import { StoreApi } from 'zustand';
import type { StringGetter } from '@itwin/appui-abstract';
import type { ToggleSwitch } from '@itwin/itwinui-react';
import { Tool } from '@itwin/core-frontend';
Expand Down Expand Up @@ -2352,6 +2353,8 @@ export class FrontstageDef {
getFloatingWidgetContainerIdByWidgetId(widgetId: string): string | undefined;
// (undocumented)
getFloatingWidgetContainerIds(): string[];
// @internal (undocumented)
getPanelsStore(): ReturnType<typeof createPanelsStore> | undefined;
// @beta
getStagePanelDef(location: StagePanelLocation): StagePanelDef | undefined;
// (undocumented)
Expand Down Expand Up @@ -2386,6 +2389,8 @@ export class FrontstageDef {
openPopoutWidgetContainer(widgetContainerId: string, oldState: NineZoneState | undefined): boolean;
// @beta
get panelDefs(): StagePanelDef[];
// (undocumented)
get panels(): FrontstagePanels;
// @beta
popoutWidget(widgetId: string, position?: XAndY, size?: SizeProps): void;
// @beta
Expand All @@ -2404,6 +2409,8 @@ export class FrontstageDef {
setFloatingWidgetContainerBounds(floatingWidgetId: string, bounds: RectangleProps): boolean;
// @internal (undocumented)
setIsApplicationClosing(value: boolean): void;
// @internal (undocumented)
setPanelsStore(panelsStore: ReturnType<typeof createPanelsStore>): void;
// (undocumented)
get statusBar(): WidgetDef | undefined;
// @internal (undocumented)
Expand Down Expand Up @@ -3450,6 +3457,9 @@ export interface OverflowToolbarOptions {
overflowExpandsTo?: Direction;
}

// @public
export type Panel = CommonPanel | InformationPanel | DynamicPanel;

// @public @deprecated
export interface PanelPinnedChangedEventArgs {
// (undocumented)
Expand Down Expand Up @@ -5246,6 +5256,8 @@ export class UiItemsManager {
// @internal
static clearAllProviders(): void;
static getBackstageItems(): ReadonlyArray<ProviderItem<BackstageItem>>;
// (undocumented)
static getPanels(stageId: string, stageUsage: string): ReadonlyArray<ProviderItem<Panel>>;
static getStatusBarItems(stageId: string, stageUsage: string): ReadonlyArray<ProviderItem<StatusBarItem>>;
static getToolbarButtonItems(stageId: string, stageUsage: string, usage: ToolbarUsage, orientation: ToolbarOrientation): ReadonlyArray<ProviderItem<ToolbarItem>>;
static getToolbarItems(stageId: string, stageUsage: string): ReadonlyArray<ProviderItem<ToolbarItem>>;
Expand All @@ -5264,6 +5276,7 @@ export class UiItemsManager {
// @public
export interface UiItemsProvider {
readonly getBackstageItems?: () => ReadonlyArray<BackstageItem>;
readonly getPanels?: () => ReadonlyArray<Panel>;
readonly getStatusBarItems?: () => ReadonlyArray<StatusBarItem>;
readonly getToolbarItems?: () => ReadonlyArray<ToolbarItem>;
readonly getWidgets?: () => ReadonlyArray<Widget>;
Expand Down
1 change: 1 addition & 0 deletions common/api/summary/appui-react.exports.csv
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ public;interface;OpenChildWindowInfo
public;class;OpenMessageCenterEvent
deprecated;class;OpenMessageCenterEvent
public;interface;OverflowToolbarOptions
public;type;Panel
public;interface;PanelPinnedChangedEventArgs
deprecated;interface;PanelPinnedChangedEventArgs
beta;class;PanelStateChangedEvent
Expand Down
155 changes: 155 additions & 0 deletions docs/storybook/src/frontstage/Panels.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import * as React from "react";
import { useActiveFrontstageDef } from "@itwin/appui-react";
import { Button } from "@itwin/itwinui-react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { action } from "storybook/actions";
import { Page } from "../AppUiStory";
import { PanelsStory } from "./Panels";
import { createWidget, removeProperty } from "../Utils";

const meta = {
title: "Frontstage/Panels",
component: PanelsStory,
tags: ["autodocs"],
parameters: {
docs: {
page: () => <Page />,
},
layout: "fullscreen",
},
args: {
getItemProvider: ({}) => {
return {
id: "items",
};
},
},
argTypes: {
getItemProvider: removeProperty(),
},
} satisfies Meta<typeof PanelsStory>;

export default meta;
type Story = StoryObj<typeof meta>;

export const DynamicPanel: Story = {
args: {
getItemProvider: () => {
return {
id: "items",
getPanels: () => [
{
id: "panel1",
content: <>Panel 1 content</>,
type: "dynamic",
placement: "left",
label: "Dynamic panel 1",
},
{
id: "panel2",
content: <>Panel 2 content</>,
type: "dynamic",
placement: "right",
label: "Dynamic panel 2",
},
{
id: "panel3",
content: <>Panel 3 content</>,
type: "dynamic",
placement: "left",
label: "Dynamic panel 3",
},
],
getWidgets: () => [
createWidget(1, {
content: <Widget />,
}),
createWidget(2),
],
};
},
},
};

function useOpenPanels() {
const frontstageDef = useActiveFrontstageDef();
const subscribe = React.useCallback(
(onStoreChange: () => void) => {
if (!frontstageDef) return () => {};
return frontstageDef.panels.onPanelOpenChanged.addListener((args) => {
action("onPanelOpenChanged")(args);
onStoreChange();
});
},
[frontstageDef]
);
const getSnapshot = React.useCallback(() => {
return frontstageDef?.panels.getOpenPanels();
}, [frontstageDef]);
return React.useSyncExternalStore(subscribe, getSnapshot);
}

function Widget() {
const frontstageDef = useActiveFrontstageDef();
const openPanels = useOpenPanels();
return (
<div
style={{
padding: "var(--iui-size-xs)",
display: "flex",
flexDirection: "column",
gap: 8,
}}
>
<Button
onClick={() => {
const id = "panel1";
const isActive = openPanels?.some((p) => p === id);
if (isActive) {
frontstageDef?.panels.close({
id,
});
return;
}
frontstageDef?.panels.open({ id });
}}
>
Toggle panel1
</Button>
<Button
onClick={() => {
const id = "panel2";
const isActive = openPanels?.some((p) => p === id);
if (isActive) {
frontstageDef?.panels.close({
id,
});
return;
}
frontstageDef?.panels.open({ id });
}}
>
Toggle panel2
</Button>
<Button
onClick={() => {
const id = "panel3";
const isActive = openPanels?.some((p) => p === id);
if (isActive) {
frontstageDef?.panels.close({
id,
});
return;
}
frontstageDef?.panels.open({ id });
}}
>
Toggle panel3
</Button>
</div>
);
}
34 changes: 34 additions & 0 deletions docs/storybook/src/frontstage/Panels.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import {
StagePanelState,
UiFramework,
UiItemsProvider,
} from "@itwin/appui-react";
import { AppUiStory } from "../AppUiStory";
import { createFrontstage } from "../Utils";

interface PanelsStoryProps {
getItemProvider: (props: PanelsStoryProps) => UiItemsProvider;
}

export function PanelsStory(props: PanelsStoryProps) {
const frontstage = createFrontstage({
leftPanelProps: {
defaultState: StagePanelState.Open,
},
});
const provider = props.getItemProvider?.(props);
return (
<AppUiStory
layout="fullscreen"
frontstages={[frontstage]}
itemProviders={[provider]}
onInitialize={async () => {
UiFramework.visibility.autoHideUi = false;
}}
/>
);
}
2 changes: 2 additions & 0 deletions ui/appui-react/src/appui-react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,8 @@ export {
StandardRotationNavigationAidControl,
} from "./appui-react/navigationaids/StandardRotationNavigationAid.js";

export { Panel } from "./appui-react/panel/Panel.js";

export {
ExpandableSection,
ExpandableSectionProps,
Expand Down
Loading