Skip to content

Commit

Permalink
Add promptAtContent to ToolAssistanceField components (#1211)
Browse files Browse the repository at this point in the history
* Update docs

* Add story

* Group status bar fields

* Use ConfigurableUiContent

* Add indefinite cursorPromptTimeout

* Fix an issue with CursorPopup where a fading popup is not re-opened.

* Refactor cursor prompt.

* Close the prompt if toggled off (after toggle on tool still needs to be activated).

* Add promptAtContent prop.

* Add promptAtContent story

* rush change

* Extract API

* NextVersion

* Mark componentDidUpdate as internal to fix docs CI issue
  • Loading branch information
GerardasB authored Feb 12, 2025
1 parent 6ffcbf0 commit 0d6dc0b
Show file tree
Hide file tree
Showing 14 changed files with 419 additions and 238 deletions.
10 changes: 7 additions & 3 deletions common/api/appui-react.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1451,6 +1451,7 @@ export class CursorPopupManager {
// @internal (undocumented)
static readonly onCursorPopupFadeOutEvent: BeUiEvent<{
id: string;
show?: CursorPopupShow;
}>;
// @internal (undocumented)
static readonly onCursorPopupsChangedEvent: BeUiEvent<object>;
Expand Down Expand Up @@ -2069,7 +2070,7 @@ export interface FrameworkKeyboardShortcuts {
export const FrameworkReducer: Reducer_2< {
configurableUiState: ConfigurableUiState;
sessionState: DeepReadonlyObject_2<SessionState>;
}, SessionStateActionsUnion_2 | ConfigurableUiActionsUnion_2, Partial<{
}, ConfigurableUiActionsUnion_2 | SessionStateActionsUnion_2, Partial<{
configurableUiState: never;
sessionState: never;
}>>;
Expand Down Expand Up @@ -3838,7 +3839,7 @@ export const SessionStateActions: {
setNumItemsSelected: (numSelected: number) => ActionWithPayload_2<SessionStateActionId.SetNumItemsSelected, number>;
setIModelConnection: (iModelConnection: any) => ActionWithPayload_2<SessionStateActionId.SetIModelConnection, any>;
setSelectionScope: (activeSelectionScope: string) => ActionWithPayload_2<SessionStateActionId.SetSelectionScope, string>;
updateCursorMenu: (cursorMenuData: CursorMenuData | CursorMenuPayload) => ActionWithPayload_2<SessionStateActionId.UpdateCursorMenu, DeepReadonlyObject_2<CursorMenuPayload> | DeepReadonlyObject_2<CursorMenuData>>;
updateCursorMenu: (cursorMenuData: CursorMenuData | CursorMenuPayload) => ActionWithPayload_2<SessionStateActionId.UpdateCursorMenu, DeepReadonlyObject_2<CursorMenuData> | DeepReadonlyObject_2<CursorMenuPayload>>;
};

// @beta @deprecated
Expand Down Expand Up @@ -3876,7 +3877,7 @@ export const sessionStateMapDispatchToProps: {
setNumItemsSelected: (numSelected: number) => ActionWithPayload_2<SessionStateActionId.SetNumItemsSelected, number>;
setIModelConnection: (iModelConnection: any) => ActionWithPayload_2<SessionStateActionId.SetIModelConnection, any>;
setSelectionScope: (activeSelectionScope: string) => ActionWithPayload_2<SessionStateActionId.SetSelectionScope, string>;
updateCursorMenu: (cursorMenuData: CursorMenuData | CursorMenuPayload) => ActionWithPayload_2<SessionStateActionId.UpdateCursorMenu, DeepReadonlyObject_2<CursorMenuPayload> | DeepReadonlyObject_2<CursorMenuData>>;
updateCursorMenu: (cursorMenuData: CursorMenuData | CursorMenuPayload) => ActionWithPayload_2<SessionStateActionId.UpdateCursorMenu, DeepReadonlyObject_2<CursorMenuData> | DeepReadonlyObject_2<CursorMenuPayload>>;
};

// @public @deprecated
Expand Down Expand Up @@ -4654,6 +4655,8 @@ export class ToolAssistanceField extends React_2.Component<ToolAssistanceFieldPr
constructor(p: ToolAssistanceFieldProps);
// (undocumented)
componentDidMount(): Promise<void>;
// @internal (undocumented)
componentDidUpdate(): void;
// (undocumented)
componentWillUnmount(): void;
// @internal (undocumented)
Expand All @@ -4674,6 +4677,7 @@ export interface ToolAssistanceFieldProps extends CommonProps {
defaultPromptAtCursor: boolean;
fadeOutCursorPrompt: boolean;
includePromptAtCursor: boolean;
promptAtContent?: boolean;
uiStateStorage?: UiStateStorage;
}

Expand Down
10 changes: 10 additions & 0 deletions common/changes/@itwin/appui-react/issue-1201_2025-02-10-11-01.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@itwin/appui-react",
"comment": "Add `promptAtContent` prop to `ToolAssistanceField` component.",
"type": "none"
}
],
"packageName": "@itwin/appui-react"
}
9 changes: 9 additions & 0 deletions docs/changehistory/NextVersion.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
# NextVersion <!-- omit from toc -->

- [@itwin/appui-react](#itwinappui-react)
- [Additions](#additions)
- [Changes](#changes)

## @itwin/appui-react

### Additions

- Added `useSavedState` property to `Widget` interface. By default widgets with `defaultState=Hidden` are always hidden when the layout is restored (i.e. page is reloaded). When `useSavedState` is set to `true` it will override the default behavior and force the widget to use its saved layout state instead. This is useful for widgets that are hidden by default but should be shown when the layout is restored. [#1210](https://github.com/iTwin/appui/pull/1210)
- Added `promptAtContent` prop to `ToolAssistanceField` component. When set to `true` the prompt will be displayed only when the content area (i.e. viewport) is hovered. [#1211](https://github.com/iTwin/appui/pull/1211)

### Changes

- Updated `cursorPromptTimeout` prop of `ToolAssistanceField` component to handle `Number.POSITIVE_INFINITY`, which when enabled will display the cursor prompt indefinitely. [#1211](https://github.com/iTwin/appui/pull/1211)
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const AlignComponent: Decorator = (Story) => {
};

const meta = {
title: "Components/MessageCenterField",
title: "Components/Status fields/MessageCenterField",
component: MessageCenterField,
tags: ["autodocs"],
decorators: [AlignComponent, InitializerDecorator, AppUiDecorator],
Expand Down
128 changes: 128 additions & 0 deletions docs/storybook/src/components/ToolAssistanceField.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import * as React from "react";
import type { Decorator, Meta, StoryObj } from "@storybook/react";
import {
MessageManager,
StatusBarItemUtilities,
ToolAssistanceField,
UiFramework,
} from "@itwin/appui-react";
import {
Tool,
ToolAssistance,
ToolAssistanceImage,
} from "@itwin/core-frontend";
import { AppUiStory } from "src/AppUiStory";
import { createFrontstage } from "src/Utils";

const StoryDecorator: Decorator = (Story) => {
return (
<AppUiStory
frontstages={[
createFrontstage({
hideStatusBar: false,
}),
]}
itemProviders={[
{
id: "provider-1",
getStatusBarItems: () => [
StatusBarItemUtilities.createCustomItem({
id: "tool-assistance",
content: (
<>
<Story />
<Setup />
</>
),
}),
],
},
]}
/>
);
};

function Setup() {
React.useEffect(() => {
const mainInstruction = ToolAssistance.createInstruction(
ToolAssistanceImage.CursorClick,
"Main instruction of a tool"
);

const cursorSection = ToolAssistance.createSection(
[
ToolAssistance.createInstruction(
ToolAssistanceImage.LeftClick,
"Left click to select a point"
),
ToolAssistance.createInstruction(
ToolAssistanceImage.RightClick,
"Right click to cancel"
),
],
ToolAssistance.inputsLabel
);

const touchSection = ToolAssistance.createSection(
[
ToolAssistance.createInstruction(
ToolAssistanceImage.OneTouchTap,
"Touch to select a point"
),
],
ToolAssistance.inputsLabel
);

const instructions = ToolAssistance.createInstructions(mainInstruction, [
cursorSection,
touchSection,
]);
// Tool assistance information
MessageManager.setToolAssistance(instructions);

// Icon for the tool assistance
UiFramework.frontstages.setActiveTool(
new (class extends Tool {
get iconSpec(): string {
return "icon-placeholder";
}
})()
);
}, []);
return null;
}

const meta = {
title: "Components/Status fields/ToolAssistanceField",
component: ToolAssistanceField,
tags: ["autodocs"],
decorators: [StoryDecorator],
args: {
includePromptAtCursor: true,
cursorPromptTimeout: 5000,
fadeOutCursorPrompt: true,
defaultPromptAtCursor: true,
},
} satisfies Meta<typeof ToolAssistanceField>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {};

export const AlwaysVisible: Story = {
args: {
cursorPromptTimeout: Number.POSITIVE_INFINITY,
},
};

export const PromptAtContent: Story = {
args: {
cursorPromptTimeout: Number.POSITIVE_INFINITY,
promptAtContent: true,
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import * as React from "react";
import type { CommonProps } from "@itwin/core-react";
import { Point } from "@itwin/core-react/internal";
import { ThemeProvider } from "@itwin/itwinui-react";
import { CursorInformation } from "../cursor/CursorInformation.js";
import {
CursorInformation,
useCursorInformationStore,
} from "../cursor/CursorInformation.js";
import { CursorPopupMenu } from "../cursor/cursormenu/CursorMenu.js";
import { CursorPopupRenderer } from "../cursor/cursorpopup/CursorPopupManager.js";
import { ModalDialogRenderer } from "../dialog/ModalDialogManager.js";
Expand Down Expand Up @@ -46,7 +49,9 @@ export const ConfigurableUiContext = React.createContext<
| "animateToolSettings"
| "toolAsToolSettingsLabel"
| "childWindow"
>
> & {
contentElementRef?: React.RefObject<HTMLElement>;
}
>({});

/** Properties for [[ConfigurableUiContent]]
Expand Down Expand Up @@ -94,6 +99,8 @@ export const WrapperContext = React.createContext<HTMLElement>(document.body);
*/
// eslint-disable-next-line @typescript-eslint/no-deprecated
export function ConfigurableUiContent(props: ConfigurableUiContentProps) {
const contentElementRef = React.useRef<HTMLElement>(null);

useWidgetOpacity(props.widgetOpacity);
useToolbarOpacity(props.toolbarOpacity);
const [mainElement, setMainElement] = React.useState<HTMLElement>();
Expand All @@ -112,11 +119,9 @@ export function ConfigurableUiContent(props: ConfigurableUiContentProps) {
};
}, [props.idleTimeout, props.intervalTimeout]);

const handleMouseMove = React.useCallback((e: React.MouseEvent) => {
const point = new Point(e.pageX, e.pageY);
CursorInformation.handleMouseMove(point);
}, []);

const setContentHovered = useCursorInformationStore(
(state) => state.setContentHovered
);
return (
<ConfigurableUiContext.Provider
value={{
Expand All @@ -128,14 +133,25 @@ export function ConfigurableUiContent(props: ConfigurableUiContentProps) {
animateToolSettings: props.animateToolSettings,
toolAsToolSettingsLabel: props.toolAsToolSettingsLabel,
childWindow: props.childWindow,
contentElementRef,
}}
>
<main
role="main"
id="uifw-configurableui-wrapper"
className={props.className}
style={props.style}
onMouseMove={handleMouseMove}
onMouseMove={(e) => {
const point = new Point(e.pageX, e.pageY);
CursorInformation.handleMouseMove(point);

if (!(e.target instanceof Node)) return;
const contentHovered = contentElementRef.current?.contains(e.target);
setContentHovered(contentHovered ?? false);
}}
onMouseLeave={() => {
setContentHovered(false);
}}
ref={(el) => setMainElement(el ?? undefined)}
>
<WrapperContext.Provider value={mainElement ?? document.body}>
Expand Down
19 changes: 19 additions & 0 deletions ui/appui-react/src/appui-react/cursor/CursorInformation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
* @module Cursor
*/

import { create } from "zustand";
import { produce } from "immer";
import type { XAndY } from "@itwin/core-geometry";
import { RelativePosition, UiEvent } from "@itwin/appui-abstract";
import { Point } from "@itwin/core-react/internal";
Expand Down Expand Up @@ -195,3 +197,20 @@ export class CursorInformation {
this._cursorDirections.length = 0;
}
}

/** Returns additional information about cursor.
* @internal
*/
export const useCursorInformationStore = create<{
contentHovered: boolean;
setContentHovered: (hovered: boolean) => void;
}>((set) => ({
contentHovered: false,
setContentHovered: (hovered: boolean) => {
set((state) =>
produce(state, (draft) => {
draft.contentHovered = hovered;
})
);
},
}));
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export class CursorPopup extends React.Component<
> = (args) => {
if (this.props.id === args.id) {
if (this._isMounted)
this.setState({ showPopup: CursorPopupShow.FadeOut });
this.setState({ showPopup: args.show ?? CursorPopupShow.FadeOut });
}
};

Expand Down
Loading

0 comments on commit 0d6dc0b

Please sign in to comment.