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
5 changes: 5 additions & 0 deletions .changeset/bottom-toolbar-positions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@itwin/appui-react": minor
---

Added `BottomContentManipulation` and `BottomViewNavigation` values to the `ToolbarUsage` enum. The standard layout now automatically renders bottom toolbar positions when a `UiItemsProvider` returns items with these usages — no custom overlay component is needed.
25 changes: 24 additions & 1 deletion common/api/appui-react.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,21 @@ export interface BasicToolWidgetProps {
showCategoryAndModelsContextTools?: boolean;
}

// @public
export function BottomContentToolWidgetComposer(): React_2.JSX.Element;

// @public
export function BottomToolWidgetComposer(props: BottomToolWidgetComposerProps): React_2.JSX.Element | null;

// @public
export interface BottomToolWidgetComposerProps {
horizontalToolbar?: React_2.ReactNode;
verticalToolbar?: React_2.ReactNode;
}

// @public
export function BottomViewToolWidgetComposer(): React_2.JSX.Element;

// @alpha
export class BumpToolSetting extends Tool {
// (undocumented)
Expand Down Expand Up @@ -2276,7 +2291,9 @@ export interface FrontstageActivatedEventArgs {

// @public @deprecated
export interface FrontstageConfig extends CommonProps {
readonly bottomContentManipulation?: WidgetConfig;
readonly bottomPanel?: StagePanelConfig;
readonly bottomViewNavigation?: WidgetConfig;
readonly contentGroup: ContentGroup | ContentGroupProvider;
readonly contentManipulation?: WidgetConfig;
readonly defaultTool?: string;
Expand Down Expand Up @@ -2317,7 +2334,11 @@ export class FrontstageDef {
// @internal
batch(fn: () => void): void;
// (undocumented)
get bottomContentManipulation(): WidgetDef | undefined;
// (undocumented)
get bottomPanel(): StagePanelDef | undefined;
// (undocumented)
get bottomViewNavigation(): WidgetDef | undefined;
// @deprecated
get contentControls(): ContentControl[];
// (undocumented)
Expand Down Expand Up @@ -4958,6 +4979,8 @@ export interface ToolbarProps extends CommonProps, NoChildrenProps {

// @public
export enum ToolbarUsage {
BottomContentManipulation = 2,
BottomViewNavigation = 3,
ContentManipulation = 0,
ViewNavigation = 1
}
Expand Down Expand Up @@ -5427,7 +5450,7 @@ export function useUiStateStorageHandler(): UiStateStorage;
// @public
export function useWidget(): {
state: WidgetState;
widgetLocation: "popout" | "docked" | "floating";
widgetLocation: "docked" | "floating" | "popout";
setState: (widgetState: Omit<WidgetState, WidgetState.Floating>) => void;
};

Expand Down
4 changes: 4 additions & 0 deletions common/api/summary/appui-react.exports.csv
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ deprecated;interface;BasicNavigationWidgetProps
public;function;BasicToolWidget
public;interface;BasicToolWidgetProps
deprecated;interface;BasicToolWidgetProps
public;function;BottomContentToolWidgetComposer
public;function;BottomToolWidgetComposer
public;interface;BottomToolWidgetComposerProps
public;function;BottomViewToolWidgetComposer
alpha;class;BumpToolSetting
public;class;Calculator
public;class;CalculatorPopup
Expand Down
6 changes: 6 additions & 0 deletions ui/appui-react/src/appui-react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,12 @@ export {
ContentToolWidgetComposer,
ContentToolWidgetComposerProps,
} from "./appui-react/widgets/ContentToolWidgetComposer.js";
export { BottomContentToolWidgetComposer } from "./appui-react/widgets/BottomContentToolWidgetComposer.js";
export {
BottomToolWidgetComposer,
BottomToolWidgetComposerProps,
} from "./appui-react/widgets/BottomToolWidgetComposer.js";
export { BottomViewToolWidgetComposer } from "./appui-react/widgets/BottomViewToolWidgetComposer.js";
export {
NavigationAidHost,
NavigationWidgetComposer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export interface FrontstageConfig extends CommonProps {
};
/** The top-right corner that shows view navigation tools. */
readonly viewNavigation?: WidgetConfig;
/** The bottom-left corner that shows tools typically used to query and modify content. */
readonly bottomContentManipulation?: WidgetConfig;
/** The bottom-right corner that shows view navigation tools. */
readonly bottomViewNavigation?: WidgetConfig;
/** The status bar of the application. */
readonly statusBar?: WidgetConfig;

Expand Down
16 changes: 16 additions & 0 deletions ui/appui-react/src/appui-react/frontstage/FrontstageDef.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ export class FrontstageDef {
private _statusBar?: WidgetDef;
private _contentManipulation?: WidgetDef;
private _viewNavigation?: WidgetDef;
private _bottomContentManipulation?: WidgetDef;
private _bottomViewNavigation?: WidgetDef;
private _topPanel?: StagePanelDef;
private _leftPanel?: StagePanelDef;
private _rightPanel?: StagePanelDef;
Expand Down Expand Up @@ -130,6 +132,12 @@ export class FrontstageDef {
public get viewNavigation(): WidgetDef | undefined {
return this._viewNavigation;
}
public get bottomContentManipulation(): WidgetDef | undefined {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if these APIs are useful on the FrontstageDef? E.g. is there a cuse-case for existing frontstageDef.viewNavigation?

return this._bottomContentManipulation;
}
public get bottomViewNavigation(): WidgetDef | undefined {
return this._bottomViewNavigation;
}

public get topPanel(): StagePanelDef | undefined {
return this._topPanel;
Expand Down Expand Up @@ -600,6 +608,14 @@ export class FrontstageDef {
config.viewNavigation,
WidgetType.Navigation
);
this._bottomContentManipulation = createWidgetDef(
config.bottomContentManipulation,
WidgetType.Tool
);
this._bottomViewNavigation = createWidgetDef(
config.bottomViewNavigation,
WidgetType.Navigation
);
this._topPanel = createStagePanelDef(config, StagePanelLocation.Top);
this._leftPanel = createStagePanelDef(config, StagePanelLocation.Left);
this._rightPanel = createStagePanelDef(config, StagePanelLocation.Right);
Expand Down
15 changes: 13 additions & 2 deletions ui/appui-react/src/appui-react/toolbar/ToolbarComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -257,10 +257,20 @@ export function ToolbarComposer(props: ExtensibleToolbarProps) {

function toExpandsTo(orientation: ToolbarOrientation, usage: ToolbarUsage) {
if (orientation === ToolbarOrientation.Vertical) {
if (usage === ToolbarUsage.ViewNavigation) return Direction.Left;
if (
usage === ToolbarUsage.ViewNavigation ||
usage === ToolbarUsage.BottomViewNavigation
)
return Direction.Left;
return Direction.Right;
}

if (
usage === ToolbarUsage.BottomContentManipulation ||
usage === ToolbarUsage.BottomViewNavigation
)
return Direction.Top;

return Direction.Bottom;
}

Expand All @@ -270,7 +280,8 @@ function toPanelAlignment(
) {
if (
orientation === ToolbarOrientation.Horizontal &&
usage === ToolbarUsage.ViewNavigation
(usage === ToolbarUsage.ViewNavigation ||
usage === ToolbarUsage.BottomViewNavigation)
)
return ToolbarPanelAlignment.End;

Expand Down
4 changes: 4 additions & 0 deletions ui/appui-react/src/appui-react/toolbar/ToolbarItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export enum ToolbarUsage {
ContentManipulation = 0,
/** Manipulate view/camera - in AppUI this is in top right of content area. */
ViewNavigation = 1,
/** Contains tools to Create Update and Delete content - rendered in bottom left of content area. */

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The enum itself should clearly be marked as non-exhaustive - new values might be added in the future.

BottomContentManipulation = 2,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we avoid placement keywords, like "Bottom"?
The "usage" should naturally help with grouping related toolbar items based on their behavior.
I went with "ViewSettings" in #1474, but other suggestions are welcome.

Alternatively, we could introduce an explicit "placement" property, however that is basically re-implementing #1474

/** Manipulate view/camera - rendered in bottom right of content area. */
Comment on lines +25 to +27

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new enum doc comment reads awkwardly: "Create Update and Delete" should be lowercased and punctuated (e.g., "create, update, and delete") for clarity and consistency with the surrounding comments.

Copilot uses AI. Check for mistakes.
BottomViewNavigation = 3,
}

/** Used to specify the orientation of the toolbar.
Expand Down
47 changes: 47 additions & 0 deletions ui/appui-react/src/appui-react/widget-panels/Toolbars.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,59 @@
); // TODO: Change this to an AppUI global CSS variable.
box-sizing: border-box;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto 1fr auto;

Comment on lines 13 to 15

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

grid-template-rows: auto 1fr auto combined with putting the existing .nz-tools-widget instances in grid-row: 1 risks breaking layout: .nz-tools-widget (ToolsArea/NavigationArea) is styled with height: 100% and relies on spanning the full content height. In an auto first row this can either force the first row to consume the full height (pushing the bottom toolbars row out of view) or prevent the tools widget from spanning full height. Consider keeping the toolbars overlay in a single grid row and anchoring bottom toolbars via align-self: end (or similar), or otherwise ensure the top .nz-tools-widget elements still span the full available height while leaving room for the bottom toolbars.

Copilot uses AI. Check for mistakes.
.nz-tools-widget {
grid-column: 1;
grid-row: 1;

&.nz-widget-navigationArea {
grid-column: 2;
grid-row: 1;
}
}

.uifw-widgetPanels-bottomToolbars {
grid-column: 1 / -1;
grid-row: 3;
display: flex;
justify-content: space-between;
align-items: flex-end;
pointer-events: none;

> * {
pointer-events: auto;
}
}

.uifw-bottom-toolArea {
display: grid;
grid-gap: 6px;
grid-template-areas:
"vtools ."
"vtools htools";
grid-template-columns: auto 1fr;
grid-template-rows: 1fr auto;
align-items: end;
justify-items: start;
}

.uifw-bottom-toolArea_vertical {
grid-area: vtools;
display: inline-flex;
flex-direction: column;
}

.uifw-bottom-toolArea_horizontal {
grid-area: htools;
min-width: 0;
}

.uifw-bottom-toolArea_right {
margin-left: auto;

.uifw-bottom-toolArea {
justify-items: end;
}
Comment on lines +38 to 67

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .uifw-bottom-toolArea* styles are nested under .uifw-widgetPanels-toolbars, so BottomToolWidgetComposer will only be styled when rendered as a descendant of WidgetPanelsToolbars. Since BottomToolWidgetComposer is exported as a public component, consider moving these rules to a top-level selector (or a dedicated SCSS file imported by the composer) so it renders correctly when used outside the standard layout.

Suggested change
.uifw-bottom-toolArea {
display: grid;
grid-gap: 6px;
grid-template-areas:
"vtools ."
"vtools htools";
grid-template-columns: auto 1fr;
grid-template-rows: 1fr auto;
align-items: end;
justify-items: start;
}
.uifw-bottom-toolArea_vertical {
grid-area: vtools;
display: inline-flex;
flex-direction: column;
}
.uifw-bottom-toolArea_horizontal {
grid-area: htools;
min-width: 0;
}
.uifw-bottom-toolArea_right {
margin-left: auto;
.uifw-bottom-toolArea {
justify-items: end;
}
}
.uifw-bottom-toolArea {
display: grid;
grid-gap: 6px;
grid-template-areas:
"vtools ."
"vtools htools";
grid-template-columns: auto 1fr;
grid-template-rows: 1fr auto;
align-items: end;
justify-items: start;
}
.uifw-bottom-toolArea_vertical {
grid-area: vtools;
display: inline-flex;
flex-direction: column;
}
.uifw-bottom-toolArea_horizontal {
grid-area: htools;
min-width: 0;
}
.uifw-bottom-toolArea_right {
margin-left: auto;
.uifw-bottom-toolArea {
justify-items: end;

Copilot uses AI. Check for mistakes.
}
}
12 changes: 12 additions & 0 deletions ui/appui-react/src/appui-react/widget-panels/Toolbars.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,22 @@ export function WidgetPanelsToolbars() {
const frontstageDef = useActiveFrontstageDef();
const tools = frontstageDef?.contentManipulation?.reactNode;
const navigation = frontstageDef?.viewNavigation?.reactNode;
const bottomTools = frontstageDef?.bottomContentManipulation?.reactNode;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

frontstageDef?.initialConfig can be used, if we get rid of bottomContentManipulation

const bottomNavigation = frontstageDef?.bottomViewNavigation?.reactNode;
return (
<div className="uifw-widgetPanels-toolbars">
{tools}
<NavigationWidget>{navigation}</NavigationWidget>
{(bottomTools || bottomNavigation) && (
<div className="uifw-widgetPanels-bottomToolbars">
{bottomTools}
{bottomNavigation && (
<div className="uifw-bottom-toolArea_right">
{bottomNavigation}
</div>
)}
</div>
)}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Widget
*/

import * as React from "react";
import { ToolbarComposer } from "../toolbar/ToolbarComposer.js";
import { BottomToolWidgetComposer } from "./BottomToolWidgetComposer.js";
import { ToolbarOrientation, ToolbarUsage } from "../toolbar/ToolbarItem.js";

/**
* BottomContentToolWidgetComposer composes a bottom-left Tool Widget with no tools defined by default.
* UiItemsProviders are used to populate the toolbars by providing items with
* `ToolbarUsage.BottomContentManipulation`.
* @public
*/
export function BottomContentToolWidgetComposer() {
return (
<BottomToolWidgetComposer
horizontalToolbar={
<ToolbarComposer
items={[]}
usage={ToolbarUsage.BottomContentManipulation}
orientation={ToolbarOrientation.Horizontal}
/>
}
verticalToolbar={
<ToolbarComposer
items={[]}
usage={ToolbarUsage.BottomContentManipulation}
orientation={ToolbarOrientation.Vertical}
/>
}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Widget
*/

import "../widget-panels/Toolbars.scss";
import * as React from "react";
Comment on lines +8 to +10

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BottomToolWidgetComposer imports ../widget-panels/Toolbars.scss, which also contains styling for WidgetPanelsToolbars and the overall center-content grid. This creates an unnecessary cross-module coupling and can pull unrelated layout CSS into bundles wherever the composer is used. Consider extracting the bottom toolbar area styles into a dedicated SCSS file (e.g., BottomToolWidgetComposer.scss) and importing that instead.

Copilot uses AI. Check for mistakes.
import {
useProximityToMouse,
WidgetElementSet,
WidgetOpacityContext,
} from "@itwin/core-react/internal";
import { UiFramework } from "../UiFramework.js";
import { useUiVisibility } from "../hooks/useUiVisibility.js";

/** Properties for {@link BottomToolWidgetComposer}.
* @public
*/
export interface BottomToolWidgetComposerProps {
/** Optional Horizontal Toolbar */
horizontalToolbar?: React.ReactNode;
/** Optional Vertical Toolbar */
verticalToolbar?: React.ReactNode;
}

/**
* BottomToolWidgetComposer renders an L-shaped toolbar area anchored to the bottom of the content area.
* The vertical toolbar grows upward and the horizontal toolbar is positioned at the bottom,
* offset by the vertical toolbar's width.
* @public
*/
export function BottomToolWidgetComposer(
props: BottomToolWidgetComposerProps
) {
const { horizontalToolbar, verticalToolbar } = props;
const [elementSet] = React.useState(new WidgetElementSet());
const proximityScale = useProximityToMouse(
elementSet,
UiFramework.visibility.snapWidgetOpacity
);
const uiIsVisible = useUiVisibility();

const addRef = React.useCallback<
React.ContextType<typeof WidgetOpacityContext>["addRef"]
>(
(ref) => {
elementSet.add(ref);
},
[elementSet]
);
const removeRef = React.useCallback<
React.ContextType<typeof WidgetOpacityContext>["removeRef"]
>(
(ref) => {
elementSet.delete(ref);
},
[elementSet]
);

if (!uiIsVisible) return null;

return (
<WidgetOpacityContext.Provider
value={{
addRef,
removeRef,
proximityScale,
}}
>
<div
className="uifw-bottom-toolArea"
onMouseEnter={UiFramework.visibility.handleWidgetMouseEnter}
>
<div className="uifw-bottom-toolArea_vertical">
{verticalToolbar}
</div>
<div className="uifw-bottom-toolArea_horizontal">
{horizontalToolbar}
</div>
</div>
</WidgetOpacityContext.Provider>
);
}
Loading
Loading