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
  • Loading branch information
GerardasB authored Dec 16, 2024
1 parent 594690c commit b8a94f9
Show file tree
Hide file tree
Showing 21 changed files with 151 additions and 83 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"
}
24 changes: 22 additions & 2 deletions docs/changehistory/NextVersion.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,34 @@
# NextVersion <!-- omit from toc -->

- [@itwin/appui-react](#itwinappui-react)
- [Additions](#additions)
- [Changes](#changes)
- [@itwin/components-react](#itwincomponents-react)
- [Additions](#additions)
- [@itwin/imodel-components-react](#itwinimodel-components-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();
}}
/>
),
});
```

### Changes

- Specified additional version ranges in redux related peer dependencies. `redux` version is updated from `^4.1.0` to `^4.1.0 || ^5.0.0` and `react-redux` version is updated from `^7.2.2` to `^7.2.2 || ^8.0.0 || ^9.0.0`. This enables consumers to utilize latest redux capabilities. See [redux release v5.0.0](https://github.com/reduxjs/redux/releases/tag/v5.0.0) for migration tips. [#1151](https://github.com/iTwin/appui/pull/1151)
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>
);
}
34 changes: 0 additions & 34 deletions ui/appui-react/src/appui-react/layout/widget/tools/button/Back.tsx

This file was deleted.

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
Loading

0 comments on commit b8a94f9

Please sign in to comment.