diff --git a/.changeset/afraid-signs-rush.md b/.changeset/afraid-signs-rush.md new file mode 100644 index 00000000000..d9ccf3323ff --- /dev/null +++ b/.changeset/afraid-signs-rush.md @@ -0,0 +1,20 @@ +--- +"@itwin/appui-react": minor +--- + +Added `ToolbarAdvancedUsage` non-exhaustive string literal union type used in `advancedUsage` property of `StandardLayoutToolbarItem` interface. +This property takes precedence over enum based `usage` property and allows specifying advanced toolbar item usage scenarios. +When set to `"view-settings"` it positions the toolbar item at the bottom-right of the content area in the default standard layout configuration. + +```tsx +ToolbarItemUtilities.createActionItem({ + id: "action", + layouts: { + standard: { + usage: ToolbarUsage.ViewNavigation, + advancedUsage: "view-settings", + orientation: ToolbarOrientation.Horizontal, + }, + }, +}); +``` diff --git a/common/api/appui-react.api.md b/common/api/appui-react.api.md index 8049ee60d02..3e4456053e6 100644 --- a/common/api/appui-react.api.md +++ b/common/api/appui-react.api.md @@ -1804,6 +1804,7 @@ export interface ExpandableSectionProps extends CommonProps { // @public @deprecated export interface ExtensibleToolbarProps { activeItemIds?: string[]; + advancedUsage?: ToolbarAdvancedUsage; items: ToolbarItem[]; orientation: ToolbarOrientation; usage: ToolbarUsage; @@ -3412,6 +3413,8 @@ export interface NavigationWidgetComposerProps extends CommonProps { hideNavigationAid?: boolean; horizontalToolbar?: React_2.ReactNode; navigationAidHost?: React_2.ReactNode; + secondaryHorizontalToolbar?: React_2.ReactNode; + secondaryVerticalToolbar?: React_2.ReactNode; verticalToolbar?: React_2.ReactNode; } @@ -4326,6 +4329,7 @@ export function StandardLayout(props: StandardLayoutProps): React_2.JSX.Element; // @public export interface StandardLayoutToolbarItem { + readonly advancedUsage?: ToolbarAdvancedUsage; readonly orientation: ToolbarOrientation; readonly usage: ToolbarUsage; } @@ -4755,6 +4759,9 @@ export interface ToolbarActionItem extends CommonToolbarItem { readonly parentGroupItemId?: string; } +// @public +export type ToolbarAdvancedUsage = "view-settings" | (string & {}); + // @public export class ToolbarButtonHelper { static getAppButton(): HTMLButtonElement | null; diff --git a/common/api/summary/appui-react.exports.csv b/common/api/summary/appui-react.exports.csv index 5d914f12098..e8a2c2e66b4 100644 --- a/common/api/summary/appui-react.exports.csv +++ b/common/api/summary/appui-react.exports.csv @@ -681,6 +681,7 @@ public;interface;ToolAssistanceFieldProps beta;function;Toolbar public;const;TOOLBAR_OPACITY_DEFAULT public;interface;ToolbarActionItem +public;type;ToolbarAdvancedUsage public;class;ToolbarButtonHelper public;function;ToolbarComposer public;interface;ToolbarCustomItem diff --git a/docs/storybook/src/Utils.tsx b/docs/storybook/src/Utils.tsx index 4448e2b80ff..244377af5fb 100644 --- a/docs/storybook/src/Utils.tsx +++ b/docs/storybook/src/Utils.tsx @@ -2,6 +2,7 @@ * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ +import { action } from "storybook/actions"; import type { ArgTypes } from "@storybook/react-vite"; import { ContentProps, @@ -12,6 +13,11 @@ import { StageUsage, StandardContentLayouts, StandardFrontstageProps, + ToolbarActionItem, + ToolbarGroupItem, + ToolbarItemUtilities, + ToolbarOrientation, + ToolbarUsage, Widget, } from "@itwin/appui-react"; import { SvgPlaceholder } from "@itwin/itwinui-icons-react"; @@ -110,3 +116,44 @@ export function createWidget(id: number, overrides?: Partial): Widget { ...overrides, }; } + +export function createToolbarItemFactory() { + let i = 0; + function createActionItem( + overrides?: Omit, "icon"> + ) { + const id = `item${++i}`; + const label = `Item ${i}`; + return ToolbarItemUtilities.createActionItem({ + id, + label, + icon: , + execute: () => action(label)(), + layouts: { + standard: { + usage: ToolbarUsage.ContentManipulation, + orientation: ToolbarOrientation.Horizontal, + }, + }, + ...overrides, + }); + } + + function createGroupItem( + overrides?: Omit, "icon"> + ) { + const id = `group${++i}`; + const label = `Group ${i}`; + return ToolbarItemUtilities.createGroupItem({ + id, + label, + icon: , + ...overrides, + }); + } + + return { + createActionItem, + createGroupItem, + }; +} diff --git a/docs/storybook/src/components/ToolbarComposer.stories.tsx b/docs/storybook/src/components/ToolbarComposer.stories.tsx index 5189af8d797..8de7b4a96e9 100644 --- a/docs/storybook/src/components/ToolbarComposer.stories.tsx +++ b/docs/storybook/src/components/ToolbarComposer.stories.tsx @@ -11,8 +11,6 @@ import { import { CommandItemDef, ToolItemDef, - ToolbarActionItem, - ToolbarGroupItem, ToolbarHelper, ToolbarItemUtilities, ToolbarOrientation, @@ -36,7 +34,10 @@ import placeholderIcon from "@bentley/icons-generic/icons/placeholder.svg"; import { AppUiDecorator, InitializerDecorator } from "../Decorators"; import { withResizer } from "../../.storybook/addons/Resizer"; import { createBumpEvent } from "../createBumpEvent"; -import { enumArgType } from "../Utils"; +import { + enumArgType, + createToolbarItemFactory as createItemFactory, +} from "../Utils"; import { ToolbarComposerStory } from "./ToolbarComposer"; const meta = { @@ -573,41 +574,6 @@ function createAbstractConditionalIcon() { }; } -function createItemFactory() { - let i = 0; - function createActionItem( - overrides?: Omit, "icon"> - ) { - const id = `item${++i}`; - const label = `Item ${i}`; - return ToolbarItemUtilities.createActionItem({ - id, - label, - icon: , - execute: () => action(label)(), - ...overrides, - }); - } - - function createGroupItem( - overrides?: Omit, "icon"> - ) { - const id = `group${++i}`; - const label = `Group ${i}`; - return ToolbarItemUtilities.createGroupItem({ - id, - label, - icon: , - ...overrides, - }); - } - - return { - createActionItem, - createGroupItem, - }; -} - function createItems() { const action1 = ToolbarItemUtilities.createActionItem( "item1", diff --git a/docs/storybook/src/frontstage/Toolbars.stories.tsx b/docs/storybook/src/frontstage/Toolbars.stories.tsx new file mode 100644 index 00000000000..183b464d71a --- /dev/null +++ b/docs/storybook/src/frontstage/Toolbars.stories.tsx @@ -0,0 +1,290 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { + ToolbarUsage, + ToolbarOrientation, + ToolbarItemLayouts, + WidgetState, +} from "@itwin/appui-react"; +import { Page } from "../AppUiStory"; +import { ToolbarsStory } from "./Toolbars"; +import { + createToolbarItemFactory, + createWidget, + enumArgType, + removeProperty, +} from "../Utils"; + +function getWidgets() { + return [createWidget(1, { defaultState: WidgetState.Hidden })]; +} + +const meta = { + title: "Frontstage/Toolbars", + component: ToolbarsStory, + tags: ["autodocs"], + parameters: { + docs: { + page: () => , + }, + layout: "fullscreen", + }, + args: { + usage: ToolbarUsage.ContentManipulation, + orientation: ToolbarOrientation.Horizontal, + length: 4, + hideNavigationAid: true, + cornerButton: false, + getItemProvider: ({ usage, orientation, length }) => { + return { + id: "items", + getToolbarItems: () => { + const factory = createToolbarItemFactory(); + const layouts = { + standard: { + usage, + orientation, + }, + } satisfies ToolbarItemLayouts; + return Array.from({ length }).map((_, index) => { + if (index === 1) { + return factory.createGroupItem({ + layouts, + items: [factory.createActionItem(), factory.createActionItem()], + }); + } + return factory.createActionItem({ layouts }); + }); + }, + getWidgets, + }; + }, + controlWidgetVisibility: false, + }, + argTypes: { + usage: enumArgType(ToolbarUsage), + orientation: enumArgType(ToolbarOrientation), + getItemProvider: removeProperty(), + contentManipulationHorizontalLength: removeProperty(), + contentManipulationVerticalLength: removeProperty(), + viewNavigationHorizontalLength: removeProperty(), + viewNavigationVerticalLength: removeProperty(), + viewSettingsHorizontalLength: removeProperty(), + viewSettingsVerticalLength: removeProperty(), + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const ContentManipulation: Story = {}; + +export const ContentManipulationVertical: Story = { + name: "Content Manipulation (vertical)", + args: { + ...ContentManipulation.args, + orientation: ToolbarOrientation.Vertical, + }, +}; + +export const ViewNavigation: Story = { + args: { + usage: ToolbarUsage.ViewNavigation, + }, +}; + +export const ViewNavigationVertical: Story = { + name: "View Navigation (vertical)", + args: { + ...ViewNavigation.args, + orientation: ToolbarOrientation.Vertical, + }, +}; + +export const ViewSettings: Story = { + args: { + usage: ToolbarUsage.ViewNavigation, + getItemProvider: ({ usage, orientation, length }) => { + return { + id: "items", + getToolbarItems: () => { + const factory = createToolbarItemFactory(); + const layouts = { + standard: { + usage, + orientation, + advancedUsage: "view-settings", + }, + } satisfies ToolbarItemLayouts; + return Array.from({ length }).map((_, index) => { + if (index === 1) { + return factory.createGroupItem({ + layouts, + items: [factory.createActionItem(), factory.createActionItem()], + }); + } + return factory.createActionItem({ layouts }); + }); + }, + getWidgets, + }; + }, + }, + argTypes: { + usage: removeProperty(), + }, +}; + +export const ViewSettingsVertical: Story = { + name: "View Settings (vertical)", + ...ViewSettings, + args: { + ...ViewSettings.args, + orientation: ToolbarOrientation.Vertical, + }, +}; + +export const All: Story = { + args: { + getItemProvider: ({ + length, + contentManipulationHorizontalLength, + contentManipulationVerticalLength, + viewSettingsHorizontalLength, + viewSettingsVerticalLength, + viewNavigationHorizontalLength, + viewNavigationVerticalLength, + }) => { + return { + id: "items", + getToolbarItems: () => { + const factory = createToolbarItemFactory(); + const toolbars: { + layouts: ToolbarItemLayouts; + length: number; + }[] = [ + { + layouts: { + standard: { + usage: ToolbarUsage.ContentManipulation, + orientation: ToolbarOrientation.Horizontal, + }, + }, + length: contentManipulationHorizontalLength ?? length, + }, + { + layouts: { + standard: { + usage: ToolbarUsage.ContentManipulation, + orientation: ToolbarOrientation.Vertical, + }, + }, + length: contentManipulationVerticalLength ?? length, + }, + { + layouts: { + standard: { + usage: ToolbarUsage.ViewNavigation, + orientation: ToolbarOrientation.Horizontal, + }, + }, + length: viewNavigationHorizontalLength ?? length, + }, + { + layouts: { + standard: { + usage: ToolbarUsage.ViewNavigation, + orientation: ToolbarOrientation.Vertical, + }, + }, + length: viewNavigationVerticalLength ?? length, + }, + { + layouts: { + standard: { + usage: ToolbarUsage.ViewNavigation, + orientation: ToolbarOrientation.Horizontal, + advancedUsage: "view-settings", + }, + }, + length: viewSettingsHorizontalLength ?? length, + }, + { + layouts: { + standard: { + usage: ToolbarUsage.ViewNavigation, + orientation: ToolbarOrientation.Vertical, + advancedUsage: "view-settings", + }, + }, + length: viewSettingsVerticalLength ?? length, + }, + ]; + return toolbars.flatMap((toolbar) => { + return Array.from({ length: toolbar.length }).map((_, index) => { + if (index === 1) + return factory.createGroupItem({ + layouts: toolbar.layouts, + items: [ + factory.createActionItem(), + factory.createActionItem(), + ], + }); + return factory.createActionItem({ layouts: toolbar.layouts }); + }); + }); + }, + getWidgets, + }; + }, + }, + argTypes: { + usage: removeProperty(), + orientation: removeProperty(), + }, +}; + +export const AllCustom: Story = { + name: "All (custom)", + ...All, + args: { + ...All.args, + viewSettingsVerticalLength: 8, + }, + argTypes: { + ...All.argTypes, + contentManipulationHorizontalLength: { + table: { + disable: false, + }, + }, + contentManipulationVerticalLength: { + table: { + disable: false, + }, + }, + viewNavigationHorizontalLength: { + table: { + disable: false, + }, + }, + viewNavigationVerticalLength: { + table: { + disable: false, + }, + }, + viewSettingsHorizontalLength: { + table: { + disable: false, + }, + }, + viewSettingsVerticalLength: { + table: { + disable: false, + }, + }, + }, +}; diff --git a/docs/storybook/src/frontstage/Toolbars.tsx b/docs/storybook/src/frontstage/Toolbars.tsx new file mode 100644 index 00000000000..42c7e779a7b --- /dev/null +++ b/docs/storybook/src/frontstage/Toolbars.tsx @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ +import { + BackstageAppButton, + PreviewFeatures, + PreviewFeaturesProvider, + ToolbarOrientation, + ToolbarUsage, + UiFramework, + UiItemsProvider, +} from "@itwin/appui-react"; +import { AppUiStory } from "../AppUiStory"; +import { createFrontstage } from "../Utils"; + +interface ToolbarStoryProps + extends Pick { + usage: ToolbarUsage; + orientation: ToolbarOrientation; + length: number; + getItemProvider: (props: ToolbarStoryProps) => UiItemsProvider; + hideNavigationAid: boolean; + cornerButton: boolean; + contentManipulationHorizontalLength?: number; + contentManipulationVerticalLength?: number; + viewNavigationHorizontalLength?: number; + viewNavigationVerticalLength?: number; + viewSettingsHorizontalLength?: number; + viewSettingsVerticalLength?: number; +} + +export function ToolbarsStory(props: ToolbarStoryProps) { + const frontstage = createFrontstage({ + rightPanelProps: { + sizeSpec: 250, + }, + hideNavigationAid: props.hideNavigationAid, + cornerButton: props.cornerButton ? : undefined, + }); + const provider = props.getItemProvider?.(props); + return ( + + { + UiFramework.visibility.autoHideUi = false; + }} + /> + + ); +} diff --git a/ui/appui-react/src/appui-react.ts b/ui/appui-react/src/appui-react.ts index 0a89f917d62..770e8649ab7 100644 --- a/ui/appui-react/src/appui-react.ts +++ b/ui/appui-react/src/appui-react.ts @@ -703,6 +703,7 @@ export { CommonToolbarItem, StandardLayoutToolbarItem, ToolbarActionItem, + ToolbarAdvancedUsage, ToolbarCustomItem, ToolbarGroupItem, ToolbarItem, diff --git a/ui/appui-react/src/appui-react/layout/StandardLayout.scss b/ui/appui-react/src/appui-react/layout/StandardLayout.scss index 503f2dc56d3..25bf573eadd 100644 --- a/ui/appui-react/src/appui-react/layout/StandardLayout.scss +++ b/ui/appui-react/src/appui-react/layout/StandardLayout.scss @@ -35,6 +35,7 @@ > .nz-centerContent { grid-area: cc; + min-height: 0; pointer-events: none; } diff --git a/ui/appui-react/src/appui-react/layout/widget/NavigationArea.scss b/ui/appui-react/src/appui-react/layout/widget/NavigationArea.scss index c245710f96d..6492f36e34e 100644 --- a/ui/appui-react/src/appui-react/layout/widget/NavigationArea.scss +++ b/ui/appui-react/src/appui-react/layout/widget/NavigationArea.scss @@ -5,65 +5,105 @@ @use "./tools/button/variables" as *; @use "../widgetopacity" as *; -.nz-widget-navigationArea { - transition: visibility var(--iui-duration-2) ease, - opacity var(--iui-duration-2) ease; - display: grid; - box-sizing: border-box; - height: 100%; - grid-gap: 6px; - grid-template-areas: - "htools button" - ". vtools"; - grid-template-columns: 1fr auto; - grid-template-rows: auto 1fr; - justify-items: end; - - &.nz-hidden { - opacity: 0; - visibility: hidden; - } +@layer appui.component { + $gap: 6px; - > .nz-navigation-aid-container { - grid-area: button; + .nz-widget-navigationArea { + transition: visibility var(--iui-duration-2) ease, + opacity var(--iui-duration-2) ease; + display: grid; + box-sizing: border-box; + height: 100%; + grid-gap: $gap; + grid-template-columns: [horizontal-start] 1fr [horizontal-end aid-start] auto [aid-end]; + grid-template-rows: [aid-start] auto [aid-end vertical-start] 1fr [vertical-end secondary-start] auto [secondary-end]; + justify-items: end; + min-height: 0; + grid-column: 2; - min-width: $mls-navigation-aid-width; - min-height: $mls-navigation-aid-height; + &.nz-hidden { + opacity: 0; + visibility: hidden; + } - margin-bottom: 6px; - pointer-events: auto; + &:not( + :has(.nz-widget-navigationArea_verticalContainer .uifw-start button) + ) { + .nz-widget-navigationArea_horizontalContainer { + grid-column-end: aid-end; + } - @include nz-widget-opacity; - } + .nz-widget-navigationArea_verticalContainer { + grid-row-start: aid-end; + } + } - > .nz-vertical-toolbar-container { - grid-area: vtools; + &:not(:has(.nz-widget-navigationArea_verticalContainer .uifw-end button)) { + .nz-widget-navigationArea_secondaryContainer { + grid-column-end: aid-end; + } - height: calc(100% - 60px); - display: inline-flex; - flex-direction: column; - align-items: flex-end; - @include nz-widget-opacity; - - &.nz-span { - grid-row: button / vtools; + .nz-widget-navigationArea_verticalContainer { + grid-row-end: vertical-end; + } } } - > .nz-horizontal-toolbar-container { - grid-area: htools; + .nz-widget-navigationArea_horizontalContainer { + grid-column: horizontal; + grid-row: aid; width: 100%; display: flex; flex-direction: row; justify-content: flex-end; min-width: 0; + gap: $gap; @include nz-widget-opacity; + &.nz-navigation-aid { + grid-column-end: aid-end; + } + > .components-toolbar-overflow-sizer { display: flex; justify-content: flex-end; } } + + .nz-widget-navigationArea_verticalContainer { + grid-column: aid; + grid-row: aid-start / secondary-end; + + display: inline-flex; + flex-direction: column; + align-items: flex-end; + grid-gap: $gap; + + @include nz-widget-opacity; + + &.nz-navigation-aid { + grid-row-start: aid-end; + } + } + + .nz-widget-navigationArea_secondaryContainer { + grid-column: horizontal; + grid-row: secondary; + + width: 100%; + min-width: 0; + + @include nz-widget-opacity; + } + + .nz-widget-navigationArea_navigationAid { + min-width: $mls-navigation-aid-width; + min-height: $mls-navigation-aid-height; + + margin-bottom: $gap; + pointer-events: auto; + flex-shrink: 0; + } } diff --git a/ui/appui-react/src/appui-react/layout/widget/NavigationArea.tsx b/ui/appui-react/src/appui-react/layout/widget/NavigationArea.tsx index 1816b3b3d24..ad92e47ce18 100644 --- a/ui/appui-react/src/appui-react/layout/widget/NavigationArea.tsx +++ b/ui/appui-react/src/appui-react/layout/widget/NavigationArea.tsx @@ -9,22 +9,21 @@ import "./NavigationArea.scss"; import classnames from "classnames"; import * as React from "react"; -import type { CommonProps, NoChildrenProps } from "@itwin/core-react"; -/** Properties of [[NavigationArea]] component. - * @internal - */ -// eslint-disable-next-line @typescript-eslint/no-deprecated -export interface NavigationAreaProps extends CommonProps, NoChildrenProps { +interface NavigationAreaProps { /** * Button displayed between horizontal and vertical toolbars. * I.e. [[AppButton]] in NavigationArea zone or navigation aid control in Navigation zone. */ navigationAid?: React.ReactNode; - /** Horizontal toolbar. See [[Toolbar]] */ + /** Horizontal toolbar. Positioned at the top-right. */ horizontalToolbar?: React.ReactNode; - /** Vertical toolbar. See [[Toolbar]] */ + /** Vertical toolbar. Positioned at the top-right. */ verticalToolbar?: React.ReactNode; + /** Secondary horizontal toolbar. Positioned at the bottom-right. */ + secondaryHorizontalToolbar?: React.ReactNode; + /** Secondary vertical toolbar. Positioned at the bottom-right. */ + secondaryVerticalToolbar?: React.ReactNode; /** Handler for mouse enter */ onMouseEnter?: (event: React.MouseEvent) => void; /** Handler for mouse leave */ @@ -36,39 +35,52 @@ export interface NavigationAreaProps extends CommonProps, NoChildrenProps { * @internal */ export function NavigationArea(props: NavigationAreaProps) { - const className = classnames( - "nz-widget-navigationArea", - props.hidden && "nz-hidden", - props.className - ); return ( -
+
{props.horizontalToolbar} + {props.navigationAid && ( +
+ {props.navigationAid} +
+ )}
- {props.navigationAid && ( -
- {props.navigationAid} -
- )}
{props.verticalToolbar} + {props.secondaryVerticalToolbar}
+ {props.secondaryHorizontalToolbar && ( +
+ {props.secondaryHorizontalToolbar} +
+ )}
); } diff --git a/ui/appui-react/src/appui-react/preview/control-widget-visibility/NavigationWidget.scss b/ui/appui-react/src/appui-react/preview/control-widget-visibility/NavigationWidget.scss index d1065c282b5..429e76794c9 100644 --- a/ui/appui-react/src/appui-react/preview/control-widget-visibility/NavigationWidget.scss +++ b/ui/appui-react/src/appui-react/preview/control-widget-visibility/NavigationWidget.scss @@ -3,15 +3,10 @@ * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ @layer appui.preview { - .uifw-preview-navigationWidget { - display: grid; - grid-gap: 6px; - grid-template-columns: 1fr; - grid-template-rows: 1fr auto; - } - .uifw-preview-navigationWidget_add { + grid-column: 2; pointer-events: all; - float: right; + + justify-self: end; } } diff --git a/ui/appui-react/src/appui-react/preview/control-widget-visibility/NavigationWidget.tsx b/ui/appui-react/src/appui-react/preview/control-widget-visibility/NavigationWidget.tsx index 3d0fe7193e0..a1cd6aceabb 100644 --- a/ui/appui-react/src/appui-react/preview/control-widget-visibility/NavigationWidget.tsx +++ b/ui/appui-react/src/appui-react/preview/control-widget-visibility/NavigationWidget.tsx @@ -18,45 +18,37 @@ import { panelSides } from "../../layout/widget-panels/Panel.js"; /** Displays a dropdown button to un-hide widgets in the bottom-right corner of the navigation widget area. * @internal */ -export function NavigationWidget({ - children, -}: React.PropsWithChildren) { +export function NavigationWidget() { const dispatch = React.useContext(NineZoneDispatchContext); const tabs = useUserControlledHiddenTabs(); const hasWidgets = useHasWidgets(); const showAdd = tabs.length > 0 && !hasWidgets; + if (!showAdd) return null; return ( -
- {children} -
- {showAdd && ( - } - menuItems={(close) => - tabs.map((tab) => { - return ( - { - dispatch({ - type: "WIDGET_TAB_SHOW", - id: tab.id, - }); - close(); - }} - > - {tab.label} - - ); - }) - } - > - {label} - - )} -
-
+ } + menuItems={(close) => + tabs.map((tab) => { + return ( + { + dispatch({ + type: "WIDGET_TAB_SHOW", + id: tab.id, + }); + close(); + }} + > + {tab.label} + + ); + }) + } + > + {label} + ); } diff --git a/ui/appui-react/src/appui-react/toolbar/ToolbarComposer.tsx b/ui/appui-react/src/appui-react/toolbar/ToolbarComposer.tsx index 568b72ad53a..ce14e6ffd80 100644 --- a/ui/appui-react/src/appui-react/toolbar/ToolbarComposer.tsx +++ b/ui/appui-react/src/appui-react/toolbar/ToolbarComposer.tsx @@ -19,6 +19,7 @@ import { ToolbarDragInteractionContext } from "./DragInteraction.js"; import type { CommonToolbarItem, ToolbarActionItem, + ToolbarAdvancedUsage, ToolbarGroupItem, ToolbarItem, } from "./ToolbarItem.js"; @@ -196,6 +197,8 @@ export interface ExtensibleToolbarProps { usage: ToolbarUsage; /** Toolbar orientation. */ orientation: ToolbarOrientation; + /** Advanced usage string to further specify the toolbar context for UI item providers. */ + advancedUsage?: ToolbarAdvancedUsage; /** Describes the ids of active toolbar items. * By default only the toolbar item with id that matches the active `Tool` id is active. * @note Property {@link CommonToolbarItem.isActiveCondition} takes precedence when determining the active state of a toolbar item. @@ -210,10 +213,19 @@ export interface ExtensibleToolbarProps { */ // eslint-disable-next-line @typescript-eslint/no-deprecated export function ToolbarComposer(props: ExtensibleToolbarProps) { - const { usage, orientation, activeItemIds: activeItemIdsProp } = props; + const { + usage, + orientation, + advancedUsage, + activeItemIds: activeItemIdsProp, + } = props; // process items from addon UI providers - const addonItems = useActiveStageProvidedToolbarItems(usage, orientation); + const addonItems = useActiveStageProvidedToolbarItems( + usage, + orientation, + advancedUsage + ); const allItems = React.useMemo(() => { return combineItems(props.items, addonItems); @@ -236,8 +248,8 @@ export function ToolbarComposer(props: ExtensibleToolbarProps) { const useProximityOpacity = useProximityOpacitySetting(); const snapWidgetOpacity = useSnapWidgetOpacitySetting(); - const expandsTo = toExpandsTo(orientation, usage); - const panelAlignment = toPanelAlignment(orientation, usage); + const expandsTo = toExpandsTo(orientation, usage, advancedUsage); + const panelAlignment = toPanelAlignment(orientation, usage, advancedUsage); return ( { + const itemUsage = item.layouts?.standard?.advancedUsage; + return itemUsage === advancedUsage; + }); } /** @@ -41,13 +46,14 @@ function getItems( */ export const useActiveStageProvidedToolbarItems = ( usage: ToolbarUsage, - orientation: ToolbarOrientation + orientation: ToolbarOrientation, + advancedUsage: ToolbarAdvancedUsage | undefined ): readonly ToolbarItem[] => { const uiItemsProviderIds = useAvailableUiItemsProviders(); const stageId = useActiveStageId(); const [items, setItems] = React.useState(() => - getItems(stageId, usage, orientation) + getItems(stageId, usage, orientation, advancedUsage) ); const providersRef = React.useRef(""); const currentStageRef = React.useRef(""); @@ -65,8 +71,8 @@ export const useActiveStageProvidedToolbarItems = ( currentStageRef.current = stageId; providersRef.current = uiProviders; - const newItems = getItems(stageId, usage, orientation); + const newItems = getItems(stageId, usage, orientation, advancedUsage); setItems(newItems); - }, [orientation, stageId, uiItemsProviderIds, usage]); + }, [orientation, stageId, uiItemsProviderIds, usage, advancedUsage]); return items; }; diff --git a/ui/appui-react/src/appui-react/toolbar/useUiItemsProviderToolbarItems.tsx b/ui/appui-react/src/appui-react/toolbar/useUiItemsProviderToolbarItems.tsx index 08121c06404..645a0fc54d4 100644 --- a/ui/appui-react/src/appui-react/toolbar/useUiItemsProviderToolbarItems.tsx +++ b/ui/appui-react/src/appui-react/toolbar/useUiItemsProviderToolbarItems.tsx @@ -27,7 +27,8 @@ export const useUiItemsProviderToolbarItems = ( ): readonly ToolbarItem[] => { const providedItems = useActiveStageProvidedToolbarItems( toolbarUsage, - toolbarOrientation + toolbarOrientation, + undefined ); const [items, setItems] = React.useState(providedItems); React.useEffect(() => { diff --git a/ui/appui-react/src/appui-react/widget-panels/Toolbars.scss b/ui/appui-react/src/appui-react/widget-panels/Toolbars.scss index 13b80312987..2e8c6bfc1e4 100644 --- a/ui/appui-react/src/appui-react/widget-panels/Toolbars.scss +++ b/ui/appui-react/src/appui-react/widget-panels/Toolbars.scss @@ -3,20 +3,15 @@ * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ .uifw-widgetPanels-toolbars { + $gap: 6px; + display: grid; + grid-gap: $gap; + grid-template-columns: 1fr 1fr; + grid-auto-rows: 1fr auto; + height: 100%; width: 100%; - padding: var( - --iui-size-s - ); // TODO: Change this to an AppUI global CSS variable. + padding: var(--iui-size-s); box-sizing: border-box; - grid-template-columns: 1fr 1fr; - - .nz-tools-widget { - grid-column: 1; - - &.nz-widget-navigationArea { - grid-column: 2; - } - } } diff --git a/ui/appui-react/src/appui-react/widget-panels/Toolbars.tsx b/ui/appui-react/src/appui-react/widget-panels/Toolbars.tsx index 00cb40bf9b8..5fd780a51c5 100644 --- a/ui/appui-react/src/appui-react/widget-panels/Toolbars.tsx +++ b/ui/appui-react/src/appui-react/widget-panels/Toolbars.tsx @@ -19,7 +19,8 @@ export function WidgetPanelsToolbars() { return (
{tools} - {navigation} + {navigation} +
); } diff --git a/ui/appui-react/src/appui-react/widgets/NavigationWidgetComposer.tsx b/ui/appui-react/src/appui-react/widgets/NavigationWidgetComposer.tsx index a7cfcb18513..34ec8a81b95 100644 --- a/ui/appui-react/src/appui-react/widgets/NavigationWidgetComposer.tsx +++ b/ui/appui-react/src/appui-react/widgets/NavigationWidgetComposer.tsx @@ -205,10 +205,14 @@ function DefaultNavigationAid() { */ // eslint-disable-next-line @typescript-eslint/no-deprecated export interface NavigationWidgetComposerProps extends CommonProps { - /** Optional horizontal toolbar */ + /** Optional horizontal toolbar. Positioned at the top-right. */ horizontalToolbar?: React.ReactNode; - /** Optional vertical toolbar */ + /** Optional vertical toolbar. Positioned at the top-right. */ verticalToolbar?: React.ReactNode; + /** Optional secondary horizontal toolbar. Positioned at the bottom-right. */ + secondaryHorizontalToolbar?: React.ReactNode; + /** Optional secondary vertical toolbar. Positioned at the bottom-right. */ + secondaryVerticalToolbar?: React.ReactNode; /** Optional navigation aid to override the default {@link NavigationAidHost}. */ navigationAidHost?: React.ReactNode; /** If true no navigation aid will be shown. Defaults to `false`. */ @@ -226,6 +230,8 @@ export function NavigationWidgetComposer(props: NavigationWidgetComposerProps) { navigationAidHost, horizontalToolbar, verticalToolbar, + secondaryHorizontalToolbar, + secondaryVerticalToolbar, hideNavigationAid, ...otherProps } = props; @@ -268,9 +274,11 @@ export function NavigationWidgetComposer(props: NavigationWidgetComposerProps) { navigationAid={navigationAid} horizontalToolbar={horizontalToolbar} verticalToolbar={verticalToolbar} - {...otherProps} + secondaryHorizontalToolbar={secondaryHorizontalToolbar} + secondaryVerticalToolbar={secondaryVerticalToolbar} onMouseEnter={UiFramework.visibility.handleWidgetMouseEnter} hidden={!uiIsVisible} + {...otherProps} /> ); diff --git a/ui/appui-react/src/appui-react/widgets/ViewToolWidgetComposer.tsx b/ui/appui-react/src/appui-react/widgets/ViewToolWidgetComposer.tsx index 919e58a27f4..91949214d8f 100644 --- a/ui/appui-react/src/appui-react/widgets/ViewToolWidgetComposer.tsx +++ b/ui/appui-react/src/appui-react/widgets/ViewToolWidgetComposer.tsx @@ -56,6 +56,22 @@ export function ViewToolWidgetComposer(props: ViewToolWidgetComposerProps) { orientation={ToolbarOrientation.Vertical} /> } + secondaryHorizontalToolbar={ + + } + secondaryVerticalToolbar={ + + } /> ); } diff --git a/ui/appui-react/src/test/layout/widget/NavigationArea.test.tsx b/ui/appui-react/src/test/layout/widget/NavigationArea.test.tsx index ca2a02a33bf..452d33985ca 100644 --- a/ui/appui-react/src/test/layout/widget/NavigationArea.test.tsx +++ b/ui/appui-react/src/test/layout/widget/NavigationArea.test.tsx @@ -5,47 +5,10 @@ import { render, screen } from "@testing-library/react"; import * as React from "react"; import { NavigationArea } from "../../../appui-react/layout/widget/NavigationArea.js"; -import { childStructure, selectorMatches } from "../Utils.js"; describe("", () => { - it("renders correctly without app button", () => { - const { container } = render(); - - expect(container.firstElementChild).to.satisfy( - childStructure( - ".nz-widget-navigationArea .nz-horizontal-toolbar-container + .nz-vertical-toolbar-container" - ) - ); - }); - - it("renders correctly with navigation aid", () => { - render(NavigationAid} />); - - expect(screen.getByText("NavigationAid")).to.satisfy( - selectorMatches( - ".nz-horizontal-toolbar-container + .nz-navigation-aid-container > div" - ) - ); - }); - - it("renders correctly with vertical toolbar", () => { - render(VerticalToolbar} />); - - expect(screen.getByText("VerticalToolbar")).to.satisfy( - selectorMatches(".nz-vertical-toolbar-container > div") - ); - }); - - it("renders correctly with horizontal toolbar", () => { - render(HorizontalToolbar} />); - - expect(screen.getByText("HorizontalToolbar")).to.satisfy( - selectorMatches(".nz-horizontal-toolbar-container > div") - ); - }); - - it("renders correctly with vertical and horizontal toolbar", () => { - const { container } = render( + it("renders correctly with toolbars", () => { + render( NavigationAid} horizontalToolbar={
HorizontalToolbar
} @@ -53,10 +16,8 @@ describe("", () => { /> ); - expect(container.firstElementChild).to.satisfy( - childStructure( - ".nz-widget-navigationArea .nz-horizontal-toolbar-container + .nz-navigation-aid-container + .nz-vertical-toolbar-container" - ) - ); + screen.getByText("HorizontalToolbar"); + screen.getByText("NavigationAid"); + screen.getByText("VerticalToolbar"); }); }); diff --git a/ui/appui-react/src/test/widget-panels/Toolbars.test.tsx b/ui/appui-react/src/test/widget-panels/Toolbars.test.tsx index ffc19330793..3e50652d795 100644 --- a/ui/appui-react/src/test/widget-panels/Toolbars.test.tsx +++ b/ui/appui-react/src/test/widget-panels/Toolbars.test.tsx @@ -13,11 +13,11 @@ describe("WidgetPanelsToolbars", () => { const frontstageDef = new FrontstageDef(); const contentManipulationWidget = WidgetDef.create({ id: "contentManipulationWidget", - content: <>tools, + content:
tools
, }); const viewNavigationWidget = WidgetDef.create({ id: "viewNavigationWidget", - content: <>navigation, + content:
navigation
, }); vi.spyOn( UiFramework.frontstages, diff --git a/ui/appui-react/src/test/widgets/BasicNavigationWidget.test.tsx b/ui/appui-react/src/test/widgets/BasicNavigationWidget.test.tsx index 7edebf5d92a..4c88deffdd1 100644 --- a/ui/appui-react/src/test/widgets/BasicNavigationWidget.test.tsx +++ b/ui/appui-react/src/test/widgets/BasicNavigationWidget.test.tsx @@ -17,7 +17,6 @@ import { ToolbarHelper, UiFramework, } from "../../appui-react.js"; -import { childStructure } from "../TestUtils.js"; import { render } from "@testing-library/react"; describe("BasicNavigationWidget", () => { @@ -141,12 +140,7 @@ describe("BasicNavigationWidget", () => { contentControlMock.object ); - const { container } = render(); - - expect(container).to.satisfy( - childStructure( - `.nz-navigation-aid-container .uifw-standard-rotation-navigation` - ) - ); + const { getByTitle } = render(); + getByTitle("standardRotationNavigationAid.title"); }); }); diff --git a/ui/appui-react/src/test/widgets/ViewToolsWidgetComposer.test.tsx b/ui/appui-react/src/test/widgets/ViewToolsWidgetComposer.test.tsx deleted file mode 100644 index 844ff5ef010..00000000000 --- a/ui/appui-react/src/test/widgets/ViewToolsWidgetComposer.test.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Bentley Systems, Incorporated. All rights reserved. - * See LICENSE.md in the project root for license terms and full copyright notice. - *--------------------------------------------------------------------------------------------*/ -import { render } from "@testing-library/react"; -import * as React from "react"; -import { ViewToolWidgetComposer } from "../../appui-react/widgets/ViewToolWidgetComposer.js"; -import { childStructure } from "../TestUtils.js"; - -describe("ViewToolWidgetComposer", () => { - it("ViewToolWidgetComposer should render correctly", () => { - const { container } = render(); - - expect(container).to.satisfy( - childStructure([ - ".nz-widget-navigationArea .nz-navigation-aid-container div:empty", - ]) - ); - }); - - it("ViewToolWidgetComposer with no navigation aid should render correctly", () => { - const { container } = render(); - - expect(container).to.not.satisfy( - childStructure([".nz-navigation-aid-container"]) - ); - }); -});