diff --git a/common/api/appui-react.api.md b/common/api/appui-react.api.md index 4f6fc7b89f0..1fb725e65ef 100644 --- a/common/api/appui-react.api.md +++ b/common/api/appui-react.api.md @@ -48,6 +48,7 @@ import { Direction } from '@itwin/components-react'; import type { DisplayStyle3dState } from '@itwin/core-frontend'; import type { EmphasizeElementsProps } from '@itwin/core-common'; import type { GroupButton } from '@itwin/appui-abstract'; +import { IconButton } from '@itwin/itwinui-react'; import type { IconProps } from '@itwin/core-react'; import type { IconSpec } from '@itwin/core-react'; import type { Id64String } from '@itwin/core-bentley'; @@ -3182,6 +3183,9 @@ export class ModalFrontstage extends React_2.Component { render(): React_2.JSX.Element; } +// @public +export function ModalFrontstageButton(props: ModalFrontstageButtonProps): React_2.JSX.Element; + // @public @deprecated export class ModalFrontstageChangedEvent extends UiEvent { } @@ -3208,6 +3212,7 @@ export interface ModalFrontstageClosedEventArgs { export interface ModalFrontstageInfo { // (undocumented) appBarRight?: React.ReactNode; + backButton?: React.ReactNode; // (undocumented) content: React.ReactNode; // @alpha @@ -3219,6 +3224,7 @@ export interface ModalFrontstageInfo { // @public export interface ModalFrontstageProps extends CommonProps { appBarRight?: React_2.ReactNode; + backButton?: React_2.ReactNode; children?: React_2.ReactNode; closeModal: () => any; isOpen?: boolean; diff --git a/common/api/summary/appui-react.exports.csv b/common/api/summary/appui-react.exports.csv index 25d339bf969..cb17cce8b70 100644 --- a/common/api/summary/appui-react.exports.csv +++ b/common/api/summary/appui-react.exports.csv @@ -426,6 +426,7 @@ public;class;ModalDialogChangedEvent deprecated;class;ModalDialogChangedEvent public;class;ModalDialogRenderer public;class;ModalFrontstage +public;function;ModalFrontstageButton public;class;ModalFrontstageChangedEvent deprecated;class;ModalFrontstageChangedEvent public;interface;ModalFrontstageChangedEventArgs diff --git a/common/changes/@itwin/appui-react/modal-frontstage-back_2024-12-13-14-49.json b/common/changes/@itwin/appui-react/modal-frontstage-back_2024-12-13-14-49.json new file mode 100644 index 00000000000..a41dca0b720 --- /dev/null +++ b/common/changes/@itwin/appui-react/modal-frontstage-back_2024-12-13-14-49.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/appui-react", + "comment": "Add ModalFrontstageButton component.", + "type": "none" + } + ], + "packageName": "@itwin/appui-react" +} \ No newline at end of file diff --git a/docs/changehistory/NextVersion.md b/docs/changehistory/NextVersion.md index 6d9890f87ee..e826d8d7e79 100644 --- a/docs/changehistory/NextVersion.md +++ b/docs/changehistory/NextVersion.md @@ -10,6 +10,7 @@ Table of contents: - [Style sheet changes](#style-sheet-changes) - [Move iTwinUI to `peerDependencies`](#move-itwinui-to-peerdependencies) - [@itwin/appui-react](#itwinappui-react) +<<<<<<< HEAD - [Removals](#removals) - [Additions](#additions) - [Changes](#changes) @@ -144,6 +145,35 @@ AppUI packages now specify `@itwin/itwinui-react` as a [peer dependency](https:/ - Added the `childWindow` prop to the `ConfigurableUiContent` component, allowing consumers to provide a wrapper component for child windows and popout widgets. [#1058](https://github.com/iTwin/appui/pull/1058) - The `StatusBarPopover` component now accepts all props that are accepted by the `Popover` component from `@itwin/itwinui-react`. [#1068](https://github.com/iTwin/appui/pull/1068) +======= + - [Additions](#additions) + - [Changes](#changes) +- [@itwin/components-react](#itwincomponents-react) + - [Additions](#additions-1) +- [@itwin/imodel-components-react](#itwinimodel-components-react) + - [Additions](#additions-2) + +## @itwin/appui-react + +### Additions + +- Add `backButton` property to `ModalFrontstageInfo` interface to allow specifying of a custom back button for a modal frontstage. Additionally `ModalFrontstageButton` component is added to maintain visual consistency between modal frontstages. [#1156](https://github.com/iTwin/appui/pull/1156) + + ```tsx + UiFramework.frontstages.openModalFrontstage({ + ...info, + backButton: ( + { + const result = window.confirm("Are you sure you want to go back?"); + if (!result) return; + UiFramework.frontstages.closeModalFrontstage(); + }} + /> + ), + }); + ``` +>>>>>>> b8a94f948 (Add `ModalFrontstageButton` component (#1156)) ### Changes diff --git a/docs/storybook/src/frontstage/Modal.stories.tsx b/docs/storybook/src/frontstage/Modal.stories.tsx index 03da3f1c2d3..fb87a7bd570 100644 --- a/docs/storybook/src/frontstage/Modal.stories.tsx +++ b/docs/storybook/src/frontstage/Modal.stories.tsx @@ -6,6 +6,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { AppUiDecorator } from "../Decorators"; import { Page } from "../AppUiStory"; import { ModalFrontstageStory } from "./Modal"; +import { ModalFrontstageButton, UiFramework } from "@itwin/appui-react"; const meta = { title: "Frontstage/ModalFrontstage", @@ -24,3 +25,17 @@ export default meta; type Story = StoryObj; export const Basic: Story = {}; + +export const BackButton: Story = { + args: { + backButton: ( + { + const result = confirm("Are you sure you want to go back?"); + if (!result) return; + UiFramework.frontstages.closeModalFrontstage(); + }} + /> + ), + }, +}; diff --git a/docs/storybook/src/frontstage/Modal.tsx b/docs/storybook/src/frontstage/Modal.tsx index dd152e987ce..6e8049da442 100644 --- a/docs/storybook/src/frontstage/Modal.tsx +++ b/docs/storybook/src/frontstage/Modal.tsx @@ -3,6 +3,7 @@ * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import { + ModalFrontstageInfo, ToolbarItemUtilities, ToolbarOrientation, ToolbarUsage, @@ -12,8 +13,11 @@ import { SvgPlaceholder } from "@itwin/itwinui-icons-react"; import { AppUiStory } from "../AppUiStory"; import { createFrontstage } from "../Utils"; +type ModalFrontstageStoryProps = Pick; + /** [openModalFrontstage](https://www.itwinjs.org/reference/appui-react/frontstage/frameworkfrontstages/#openmodalfrontstage) can be used to open a modal frontstage. */ -export function ModalFrontstageStory() { +export function ModalFrontstageStory(props: ModalFrontstageStoryProps) { + const { backButton } = props; return ( Modal frontstage content, title: "My Modal Frontstage", + backButton, }); }, layouts: { diff --git a/e2e-tests/tests/configurableui/configurable-ui.test.ts-snapshots/configurable-ui-test-1-chromium-linux.png b/e2e-tests/tests/configurableui/configurable-ui.test.ts-snapshots/configurable-ui-test-1-chromium-linux.png index a7f2ebcf918..312a2acb69e 100644 Binary files a/e2e-tests/tests/configurableui/configurable-ui.test.ts-snapshots/configurable-ui-test-1-chromium-linux.png and b/e2e-tests/tests/configurableui/configurable-ui.test.ts-snapshots/configurable-ui-test-1-chromium-linux.png differ diff --git a/e2e-tests/tests/content/content-layout.test.ts-snapshots/content-layout-test-1-chromium-linux.png b/e2e-tests/tests/content/content-layout.test.ts-snapshots/content-layout-test-1-chromium-linux.png index 063c2008d48..e4beddc949a 100644 Binary files a/e2e-tests/tests/content/content-layout.test.ts-snapshots/content-layout-test-1-chromium-linux.png and b/e2e-tests/tests/content/content-layout.test.ts-snapshots/content-layout-test-1-chromium-linux.png differ diff --git a/e2e-tests/tests/content/split-pane.test.ts-snapshots/content-layout-test-1-chromium-linux.png b/e2e-tests/tests/content/split-pane.test.ts-snapshots/content-layout-test-1-chromium-linux.png index 230449a7d5d..345ba968c1a 100644 Binary files a/e2e-tests/tests/content/split-pane.test.ts-snapshots/content-layout-test-1-chromium-linux.png and b/e2e-tests/tests/content/split-pane.test.ts-snapshots/content-layout-test-1-chromium-linux.png differ diff --git a/e2e-tests/tests/frontstage/modal-frontstage.test.ts-snapshots/modal-frontstage-test-1-chromium-linux.png b/e2e-tests/tests/frontstage/modal-frontstage.test.ts-snapshots/modal-frontstage-test-1-chromium-linux.png index d574bba3f6d..b74b118acbb 100644 Binary files a/e2e-tests/tests/frontstage/modal-frontstage.test.ts-snapshots/modal-frontstage-test-1-chromium-linux.png and b/e2e-tests/tests/frontstage/modal-frontstage.test.ts-snapshots/modal-frontstage-test-1-chromium-linux.png differ diff --git a/e2e-tests/tests/picker/view-selector.test.ts-snapshots/view-selector-test-1-chromium-linux.png b/e2e-tests/tests/picker/view-selector.test.ts-snapshots/view-selector-test-1-chromium-linux.png index fbace9f2860..395faf7a7c7 100644 Binary files a/e2e-tests/tests/picker/view-selector.test.ts-snapshots/view-selector-test-1-chromium-linux.png and b/e2e-tests/tests/picker/view-selector.test.ts-snapshots/view-selector-test-1-chromium-linux.png differ diff --git a/ui/appui-react/src/appui-react.ts b/ui/appui-react/src/appui-react.ts index 52621b55548..601d652411a 100644 --- a/ui/appui-react/src/appui-react.ts +++ b/ui/appui-react/src/appui-react.ts @@ -289,6 +289,7 @@ export { ModalFrontstage, ModalFrontstageProps, } from "./appui-react/frontstage/ModalFrontstage.js"; +export { ModalFrontstageButton } from "./appui-react/frontstage/ModalFrontstageButton.js"; export { SettingsModalFrontstage } from "./appui-react/frontstage/ModalSettingsStage.js"; export { NestedFrontstage } from "./appui-react/frontstage/NestedFrontstage.js"; export { NestedFrontstageAppButton } from "./appui-react/frontstage/NestedFrontstageAppButton.js"; diff --git a/ui/appui-react/src/appui-react/framework/FrameworkFrontstages.ts b/ui/appui-react/src/appui-react/framework/FrameworkFrontstages.ts index 1e656e1caeb..510cade4b29 100644 --- a/ui/appui-react/src/appui-react/framework/FrameworkFrontstages.ts +++ b/ui/appui-react/src/appui-react/framework/FrameworkFrontstages.ts @@ -25,6 +25,7 @@ import type { import type { WidgetState } from "../widgets/WidgetState.js"; import type { Frontstage } from "../frontstage/Frontstage.js"; import { FrameworkContent } from "./FrameworkContent.js"; +import type { ModalFrontstageButton } from "../frontstage/ModalFrontstageButton.js"; /** Frontstage Activated Event Args interface. * @public @@ -183,6 +184,8 @@ export interface ModalFrontstageInfo { * that the stage can save unsaved data before closing. Used by the ModalSettingsStage. * @alpha */ notifyCloseRequest?: boolean; + /** If specified overrides the default back button. See {@link ModalFrontstageButton}. */ + backButton?: React.ReactNode; } /** Modal Frontstage array item interface. diff --git a/ui/appui-react/src/appui-react/frontstage/ModalFrontstage.scss b/ui/appui-react/src/appui-react/frontstage/ModalFrontstage.scss index 7d6cba00cfb..9243c000223 100644 --- a/ui/appui-react/src/appui-react/frontstage/ModalFrontstage.scss +++ b/ui/appui-react/src/appui-react/frontstage/ModalFrontstage.scss @@ -44,11 +44,6 @@ float: right; margin-right: 20px; } - - > :first-child { - display: inline-block; - border-radius: 0; // Turn off circular border from Back.scss in ui-ninezone - } } .uifw-modal-stage-content { diff --git a/ui/appui-react/src/appui-react/frontstage/ModalFrontstage.tsx b/ui/appui-react/src/appui-react/frontstage/ModalFrontstage.tsx index b164bf51c7a..a4e2ebcd155 100644 --- a/ui/appui-react/src/appui-react/frontstage/ModalFrontstage.tsx +++ b/ui/appui-react/src/appui-react/frontstage/ModalFrontstage.tsx @@ -6,14 +6,12 @@ * @module Frontstage */ +import "./ModalFrontstage.scss"; +import * as React from "react"; +import classnames from "classnames"; import type { CommonProps } from "@itwin/core-react"; -import { SvgProgressBackwardCircular } from "@itwin/itwinui-icons-react"; import { Text } from "@itwin/itwinui-react"; -import classnames from "classnames"; -import * as React from "react"; -import { UiFramework } from "../UiFramework.js"; -import { BackButton } from "../layout/widget/tools/button/Back.js"; -import "./ModalFrontstage.scss"; +import { ModalFrontstageButton } from "./ModalFrontstageButton.js"; /** Properties for the [[ModalFrontstage]] React component * @public @@ -30,6 +28,8 @@ export interface ModalFrontstageProps extends CommonProps { closeModal: () => any; /** An optional React node displayed in the upper right of the modal Frontstage. */ appBarRight?: React.ReactNode; + /** If specified overrides the default back button. */ + backButton?: React.ReactNode; /** Content */ children?: React.ReactNode; } @@ -58,12 +58,11 @@ export class ModalFrontstage extends React.Component { <>
- } - title={UiFramework.translate("modalFrontstage.backButtonTitle")} - /> + {this.props.backButton ? ( + this.props.backButton + ) : ( + + )} {this.props.title} diff --git a/ui/appui-react/src/appui-react/layout/widget/tools/button/Back.scss b/ui/appui-react/src/appui-react/frontstage/ModalFrontstageButton.scss similarity index 58% rename from ui/appui-react/src/appui-react/layout/widget/tools/button/Back.scss rename to ui/appui-react/src/appui-react/frontstage/ModalFrontstageButton.scss index d4c47e708b9..bec36e83ef6 100644 --- a/ui/appui-react/src/appui-react/layout/widget/tools/button/Back.scss +++ b/ui/appui-react/src/appui-react/frontstage/ModalFrontstageButton.scss @@ -2,8 +2,20 @@ * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ -@use "variables" as *; +.uifw-frontstage-modalFrontstageButton { + block-size: 3.5rem; + aspect-ratio: 1; -.nz-toolbar-button-back { - border-radius: $mls-button-width * 0.5; + border-radius: 0; +} + +.uifw-frontstage-modalFrontstageButton_icon { + $size: 2rem; + + font-size: $size; + + svg { + block-size: $size; + inline-size: $size; + } } diff --git a/ui/appui-react/src/appui-react/frontstage/ModalFrontstageButton.tsx b/ui/appui-react/src/appui-react/frontstage/ModalFrontstageButton.tsx new file mode 100644 index 00000000000..a003c957fed --- /dev/null +++ b/ui/appui-react/src/appui-react/frontstage/ModalFrontstageButton.tsx @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ +/** @packageDocumentation + * @module Frontstage + */ + +import "./ModalFrontstageButton.scss"; +import * as React from "react"; +import { SvgProgressBackwardCircular } from "@itwin/itwinui-icons-react"; +import { UiFramework } from "../UiFramework.js"; +import { useTranslation } from "../hooks/useTranslation.js"; +import { IconButton } from "@itwin/itwinui-react"; + +type IconButtonProps = React.ComponentProps; + +interface ModalFrontstageButtonProps extends Pick { + children?: never; + /** If specified overrides the default icon. */ + icon?: React.ReactNode; + /** If specified overrides the default label. */ + label?: string; + /** If specified overrides the default behavior of closing the modal frontstage. */ + onClick?: IconButtonProps["onClick"]; +} + +/** Button usually shown in the top-left corner of the modal frontstage. By default closes the modal frontstage. + * @public + */ +export function ModalFrontstageButton(props: ModalFrontstageButtonProps) { + const { translate } = useTranslation(); + const { label, icon, onClick } = props; + const defaultLabel = translate("modalFrontstage.backButtonTitle"); + const defaultIcon = ; + + const defaultOnClick = React.useCallback(() => { + UiFramework.frontstages.closeModalFrontstage(); + }, []); + + return ( + + {icon ?? defaultIcon} + + ); +} diff --git a/ui/appui-react/src/appui-react/widget-panels/ModalFrontstageComposer.tsx b/ui/appui-react/src/appui-react/widget-panels/ModalFrontstageComposer.tsx index b31ef415b0f..81987ee9f68 100644 --- a/ui/appui-react/src/appui-react/widget-panels/ModalFrontstageComposer.tsx +++ b/ui/appui-react/src/appui-react/widget-panels/ModalFrontstageComposer.tsx @@ -40,7 +40,7 @@ export function ModalFrontstageComposer({ ); if (!stageInfo) return null; - const { title, content, appBarRight } = stageInfo; + const { title, content, appBarRight, backButton } = stageInfo; return ( {content} diff --git a/ui/appui-react/src/test/frontstage/ModalFrontstage.test.tsx b/ui/appui-react/src/test/frontstage/ModalFrontstage.test.tsx index ffe164ff6aa..3aa6785abae 100644 --- a/ui/appui-react/src/test/frontstage/ModalFrontstage.test.tsx +++ b/ui/appui-react/src/test/frontstage/ModalFrontstage.test.tsx @@ -2,7 +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 { render } from "@testing-library/react"; +import { fireEvent, render } from "@testing-library/react"; import * as React from "react"; import type { ModalFrontstageInfo } from "../../appui-react.js"; import { ModalFrontstage, UiFramework } from "../../appui-react.js"; @@ -62,22 +62,21 @@ describe("ModalFrontstage", () => { UiFramework.frontstages.openModalFrontstage(modalFrontstage); expect(changedEventSpy).toHaveBeenCalledOnce(); - const { baseElement, rerender } = render(renderModalFrontstage(false)); + const { baseElement, rerender, getByRole } = render( + renderModalFrontstage(false) + ); rerender(renderModalFrontstage(true)); expect( baseElement.querySelectorAll("div.uifw-modal-frontstage").length ).toEqual(1); - const backButton = baseElement.querySelectorAll( - "button.nz-toolbar-button-back" - ); - expect(backButton.length).toEqual(1); + const backButton = getByRole("button"); UiFramework.frontstages.updateModalFrontstage(); expect(changedEventSpy).toHaveBeenCalledTimes(2); - backButton[0].click(); + fireEvent.click(backButton); expect(navigationBackSpy).toHaveBeenCalledOnce(); expect(closeModalSpy).toHaveBeenCalledOnce(); diff --git a/ui/appui-react/src/test/layout/widget/tools/Back.test.tsx b/ui/appui-react/src/test/layout/widget/tools/Back.test.tsx deleted file mode 100644 index 1a2c8b4e469..00000000000 --- a/ui/appui-react/src/test/layout/widget/tools/Back.test.tsx +++ /dev/null @@ -1,18 +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, screen } from "@testing-library/react"; -import * as React from "react"; -import { BackButton } from "../../../../appui-react/layout/widget/tools/button/Back.js"; -import { selectorMatches } from "../../Utils.js"; - -describe("", () => { - it("renders correctly", () => { - render(); - - expect(screen.getByRole("button")).to.satisfy( - selectorMatches(".nz-toolbar-button-back") - ); - }); -});