Skip to content

Commit

Permalink
Add ModalFrontstageButton component (#1156)
Browse files Browse the repository at this point in the history
* Ability to override backButton of a modal frontstage.

* Add ModalFrontstageButton

* Remove unused BackButton

* rush change

* NextVersion.md

* Extract API

* Update snaps

(cherry picked from commit b8a94f9)

# Conflicts:
#	docs/changehistory/NextVersion.md
#	ui/appui-react/src/appui-react/layout/widget/tools/button/Back.tsx
  • Loading branch information
GerardasB authored and mergify[bot] committed Dec 16, 2024
1 parent 282c149 commit df80eb2
Show file tree
Hide file tree
Showing 20 changed files with 159 additions and 47 deletions.
6 changes: 6 additions & 0 deletions common/api/appui-react.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -3182,6 +3183,9 @@ export class ModalFrontstage extends React_2.Component<ModalFrontstageProps> {
render(): React_2.JSX.Element;
}

// @public
export function ModalFrontstageButton(props: ModalFrontstageButtonProps): React_2.JSX.Element;

// @public @deprecated
export class ModalFrontstageChangedEvent extends UiEvent<ModalFrontstageChangedEventArgs> {
}
Expand All @@ -3208,6 +3212,7 @@ export interface ModalFrontstageClosedEventArgs {
export interface ModalFrontstageInfo {
// (undocumented)
appBarRight?: React.ReactNode;
backButton?: React.ReactNode;
// (undocumented)
content: React.ReactNode;
// @alpha
Expand All @@ -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;
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 @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@itwin/appui-react",
"comment": "Add ModalFrontstageButton component.",
"type": "none"
}
],
"packageName": "@itwin/appui-react"
}
30 changes: 30 additions & 0 deletions docs/changehistory/NextVersion.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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: (
<ModalFrontstageButton
onClick={() => {
const result = window.confirm("Are you sure you want to go back?");
if (!result) return;
UiFramework.frontstages.closeModalFrontstage();
}}
/>
),
});
```
>>>>>>> b8a94f948 (Add `ModalFrontstageButton` component (#1156))
### Changes

Expand Down
15 changes: 15 additions & 0 deletions docs/storybook/src/frontstage/Modal.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -24,3 +25,17 @@ export default meta;
type Story = StoryObj<typeof meta>;

export const Basic: Story = {};

export const BackButton: Story = {
args: {
backButton: (
<ModalFrontstageButton
onClick={() => {
const result = confirm("Are you sure you want to go back?");
if (!result) return;
UiFramework.frontstages.closeModalFrontstage();
}}
/>
),
},
};
7 changes: 6 additions & 1 deletion docs/storybook/src/frontstage/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import {
ModalFrontstageInfo,
ToolbarItemUtilities,
ToolbarOrientation,
ToolbarUsage,
Expand All @@ -12,8 +13,11 @@ import { SvgPlaceholder } from "@itwin/itwinui-icons-react";
import { AppUiStory } from "../AppUiStory";
import { createFrontstage } from "../Utils";

type ModalFrontstageStoryProps = Pick<ModalFrontstageInfo, "backButton">;

/** [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 (
<AppUiStory
layout="fullscreen"
Expand All @@ -30,6 +34,7 @@ export function ModalFrontstageStory() {
UiFramework.frontstages.openModalFrontstage({
content: <>Modal frontstage content</>,
title: "My Modal Frontstage",
backButton,
});
},
layouts: {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions ui/appui-react/src/appui-react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
23 changes: 11 additions & 12 deletions ui/appui-react/src/appui-react/frontstage/ModalFrontstage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
Expand Down Expand Up @@ -58,12 +58,11 @@ export class ModalFrontstage extends React.Component<ModalFrontstageProps> {
<>
<div className={classNames} style={this.props.style}>
<div className="uifw-modal-app-bar">
<BackButton
className="nz-toolbar-button-app"
onClick={this._onGoBack}
icon={<SvgProgressBackwardCircular />}
title={UiFramework.translate("modalFrontstage.backButtonTitle")}
/>
{this.props.backButton ? (
this.props.backButton
) : (
<ModalFrontstageButton onClick={this._onGoBack} />
)}
<Text variant="headline" className="uifw-headline">
{this.props.title}
</Text>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<typeof IconButton>;

interface ModalFrontstageButtonProps extends Pick<IconButtonProps, "onClick"> {
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 = <SvgProgressBackwardCircular />;

const defaultOnClick = React.useCallback(() => {
UiFramework.frontstages.closeModalFrontstage();
}, []);

return (
<IconButton
className="uifw-frontstage-modalFrontstageButton"
onClick={onClick ?? defaultOnClick}
label={label ?? defaultLabel}
iconProps={{
className: "uifw-frontstage-modalFrontstageButton_icon",
}}
>
{icon ?? defaultIcon}
</IconButton>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,15 @@ export function ModalFrontstageComposer({
);
if (!stageInfo) return null;

const { title, content, appBarRight } = stageInfo;
const { title, content, appBarRight, backButton } = stageInfo;

return (
<ModalFrontstage
isOpen={true}
title={title}
closeModal={handleCloseModal}
appBarRight={appBarRight}
backButton={backButton}
>
{content}
</ModalFrontstage>
Expand Down
13 changes: 6 additions & 7 deletions ui/appui-react/src/test/frontstage/ModalFrontstage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<HTMLButtonElement>(
"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();

Expand Down
18 changes: 0 additions & 18 deletions ui/appui-react/src/test/layout/widget/tools/Back.test.tsx

This file was deleted.

0 comments on commit df80eb2

Please sign in to comment.