diff --git a/src/mobile-ui-react/ActionSheetButton.tsx b/src/mobile-ui-react/ActionSheetButton.tsx index a835c89..53bbb13 100644 --- a/src/mobile-ui-react/ActionSheetButton.tsx +++ b/src/mobile-ui-react/ActionSheetButton.tsx @@ -3,16 +3,15 @@ * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import * as React from "react"; -import { CommonProps, IconSpec } from "@itwin/core-react"; +import { IconSpec } from "@itwin/core-react"; import { ActionSheetProps, presentActionSheet } from "@itwin/mobile-sdk-core"; import { NavigationButton } from "./NavigationPanel"; +import { CommonProps } from "./MobileUi"; /** * Properties for {@link ActionSheetButton} * @public */ -// @todo AppUI deprecation -// eslint-disable-next-line deprecation/deprecation export interface ActionSheetButtonProps extends ActionSheetProps, CommonProps { /** The icon to show on the {@link ActionSheetButton}, default is three vertical dots. */ iconSpec?: IconSpec; diff --git a/src/mobile-ui-react/BottomPanel.tsx b/src/mobile-ui-react/BottomPanel.tsx index 71d5e96..4c7a220 100644 --- a/src/mobile-ui-react/BottomPanel.tsx +++ b/src/mobile-ui-react/BottomPanel.tsx @@ -5,9 +5,14 @@ import * as React from "react"; import classnames from "classnames"; import { BeUiEvent } from "@itwin/core-bentley"; -import { CommonProps, getCssVariableAsNumber } from "@itwin/core-react"; -import { Optional } from "@itwin/mobile-sdk-core"; -import { makeRefHandler, MutableHtmlDivRefOrFunction, useBeUiEvent, useWindowEvent } from "./MobileUi"; +import { getCssVariableAsNumber, Optional } from "@itwin/mobile-sdk-core"; +import { + CommonProps, + makeRefHandler, + MutableHtmlDivRefOrFunction, + useBeUiEvent, + useWindowEvent, +} from "./MobileUi"; import { PanelHeader, PanelHeaderProps } from "./PanelHeader"; import { ResizablePanel, ResizablePanelProps } from "./ResizablePanel"; import "./BottomPanel.scss"; @@ -89,8 +94,6 @@ export function useBottomPanelTop() { * Properties for the {@link BottomPanel} component. * @public */ -// @todo AppUI deprecation -// eslint-disable-next-line deprecation/deprecation export interface BottomPanelProps extends CommonProps { children?: React.ReactNode; /** Displayed when true. */ @@ -295,8 +298,6 @@ export function ResizableBottomPanel(props: ResizableBottomPanelProps) { } setTimeout(() => { setFlickingDown(false); - // @todo AppUI deprecation - // eslint-disable-next-line deprecation/deprecation }, 50 + (typeof autoCloseResult === "number" ? autoCloseResult : getCssVariableAsNumber("--mui-bottom-panel-animation-duration") * 1000)); return result; }); diff --git a/src/mobile-ui-react/CenterDiv.tsx b/src/mobile-ui-react/CenterDiv.tsx index 2fa0eaa..31ca6f3 100644 --- a/src/mobile-ui-react/CenterDiv.tsx +++ b/src/mobile-ui-react/CenterDiv.tsx @@ -4,15 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as React from "react"; import classnames from "classnames"; -import { CommonProps } from "@itwin/core-react"; import "./CenterDiv.scss"; +import { CommonProps } from "./MobileUi"; /** * Properties for {@link CenterDiv} component * @public */ -// @todo AppUI deprecation -// eslint-disable-next-line deprecation/deprecation interface CenterDivProps extends CommonProps, React.DOMAttributes { /** Set to true to have the {@link CenterDiv} fill 100% of its parent. Default: no */ fill?: boolean; diff --git a/src/mobile-ui-react/CountNotification.tsx b/src/mobile-ui-react/CountNotification.tsx index 2d63d4c..b40257b 100644 --- a/src/mobile-ui-react/CountNotification.tsx +++ b/src/mobile-ui-react/CountNotification.tsx @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as React from "react"; import classnames from "classnames"; -import { CommonProps, IconSpec } from "@itwin/core-react"; +import { IconSpec } from "@itwin/core-react"; import { AlertAction } from "@itwin/mobile-sdk-core"; import { ActionSheetButton } from "./ActionSheetButton"; import { IconImage } from "./IconImage"; -import { MobileUi } from "./MobileUi"; +import { CommonProps, MobileUi } from "./MobileUi"; import "./CountNotification.scss"; @@ -40,8 +40,6 @@ export interface CountNotificationMoreProps { * Properties for {@link CountNotification} component * @public */ -// @todo AppUI deprecation -// eslint-disable-next-line deprecation/deprecation export interface CountNotificationProps extends CommonProps { /** Count to display */ count: number; @@ -67,8 +65,6 @@ export interface CountNotificationProps extends CommonProps { * Properties for {@link CloseableCountNotification} component * @public */ -// @todo AppUI deprecation -// eslint-disable-next-line deprecation/deprecation export interface CloseableCountNotificationProps extends CommonProps { /** Count to display */ count: number; diff --git a/src/mobile-ui-react/HorizontalPicker.tsx b/src/mobile-ui-react/HorizontalPicker.tsx index d3de0a3..1597a15 100644 --- a/src/mobile-ui-react/HorizontalPicker.tsx +++ b/src/mobile-ui-react/HorizontalPicker.tsx @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as React from "react"; import classnames from "classnames"; -import { CommonProps } from "@itwin/core-react"; +import { CommonProps } from "./MobileUi"; import "./HorizontalPicker.scss"; @@ -12,8 +12,6 @@ import "./HorizontalPicker.scss"; * Properties for {@link HorizontalPicker} component * @public */ -// @todo AppUI deprecation -// eslint-disable-next-line deprecation/deprecation export interface HorizontalPickerProps extends CommonProps { /** The items in the picker. */ items: React.ReactNode[]; @@ -23,8 +21,6 @@ export interface HorizontalPickerProps extends CommonProps { onItemSelected: (item: number) => void; } -// @todo AppUI deprecation -// eslint-disable-next-line deprecation/deprecation interface HorizontalPickerItemProps extends CommonProps { itemNode: React.ReactNode; onClick: () => void; diff --git a/src/mobile-ui-react/MobileUi.tsx b/src/mobile-ui-react/MobileUi.tsx index 2601180..c89a569 100644 --- a/src/mobile-ui-react/MobileUi.tsx +++ b/src/mobile-ui-react/MobileUi.tsx @@ -6,12 +6,12 @@ import * as React from "react"; import { BackendError, Localization } from "@itwin/core-common"; import { ColorTheme, - SessionStateActionId, SyncUiEventDispatcher, SyncUiEventId, SYSTEM_PREFERRED_COLOR_THEME, UiFramework, UiSyncEventArgs, + useActiveIModelConnection, } from "@itwin/appui-react"; import { EmphasizeElements, IModelApp, IModelConnection, ScreenViewport, SelectionSet, Tool, Viewport } from "@itwin/core-frontend"; import { BeEvent, BeUiEvent, BriefcaseStatus, Id64Set, Listener } from "@itwin/core-bentley"; @@ -27,6 +27,30 @@ import { import "./MobileUi.scss"; +/** Props used by components that expect class name to be passed in. + * + * __Note__: Copied from @itwin/core-react, where it is being deprecated. It will **not ever** be + * deprecated from @itwin/mobile-ui-react. + * @public + */ +export interface ClassNameProps { + /** Custom CSS class name */ + className?: string; +} + +/** Common props used by components. + * + * __Note__: Copied from @itwin/core-react, where it is being deprecated. It will **not ever** be + * deprecated from @itwin/mobile-ui-react. + * @public + */ +export interface CommonProps extends ClassNameProps { + /** Custom CSS style properties */ + style?: React.CSSProperties; + /** Optional unique identifier for item. If defined it will be added to DOM Element attribute as data-item-id */ + itemId?: string; +} + /** Type used for MobileUi.onClose BeEvent. */ export declare type CloseListener = () => void; @@ -558,11 +582,10 @@ export function useIsolatedCount(): number { * @param handler - The callback function. */ export function useIModel(handler: (iModel: IModelConnection | undefined) => void) { - useSyncUiEvent(React.useCallback(() => { - handler(UiFramework.getIModelConnection()); - // @todo AppUI deprecation - // eslint-disable-next-line deprecation/deprecation - }, [handler]), SessionStateActionId.SetIModelConnection); + const iModelConnection = useActiveIModelConnection(); + React.useMemo(() => { + handler(iModelConnection); + }, [iModelConnection, handler]); } /** diff --git a/src/mobile-ui-react/ModalEntryFormDialog.tsx b/src/mobile-ui-react/ModalEntryFormDialog.tsx index 9f74936..01a55b8 100644 --- a/src/mobile-ui-react/ModalEntryFormDialog.tsx +++ b/src/mobile-ui-react/ModalEntryFormDialog.tsx @@ -4,11 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as React from "react"; import classnames from "classnames"; -import { CommonProps } from "@itwin/core-react"; import { UiFramework } from "@itwin/appui-react"; import { getCssVariableAsNumberOrDefault, MobileCore, Optional } from "@itwin/mobile-sdk-core"; import { CloseButton } from "./NavigationPanel"; -import { MobileUi } from "./MobileUi"; +import { CommonProps, MobileUi } from "./MobileUi"; import "./ModalEntryFormDialog.scss"; @@ -41,8 +40,6 @@ export interface ModalEntryFormValue { * Properties for the {@link ModalDialog} component. * @public */ -// @todo AppUI deprecation -// eslint-disable-next-line deprecation/deprecation export interface ModalDialogProps extends CommonProps { /** Content. */ children: React.ReactNode; diff --git a/src/mobile-ui-react/NavigationPanel.tsx b/src/mobile-ui-react/NavigationPanel.tsx index 2c12f5c..f22d5f2 100644 --- a/src/mobile-ui-react/NavigationPanel.tsx +++ b/src/mobile-ui-react/NavigationPanel.tsx @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as React from "react"; import classnames from "classnames"; -import { ClassNameProps, CommonProps, IconSpec } from "@itwin/core-react"; +import { IconSpec } from "@itwin/core-react"; import { ConditionalBooleanValue, ConditionalStringValue } from "@itwin/appui-abstract"; import { IconImage } from "./IconImage"; -import { useSyncUiEvent } from "./MobileUi"; +import { ClassNameProps, CommonProps, useSyncUiEvent } from "./MobileUi"; import "./NavigationPanel.scss"; import { @@ -21,8 +21,6 @@ import { * Properties for the {@link NavigationPanel} component. * @public */ -// @todo AppUI deprecation -// eslint-disable-next-line deprecation/deprecation export interface NavigationPanelProps extends ClassNameProps { /** The left side components. */ left?: React.ReactNode; @@ -46,8 +44,6 @@ export function NavigationPanel(props: NavigationPanelProps) { * Properties for the {@link NavigationButton} component. * @public */ -// @todo AppUI deprecation -// eslint-disable-next-line deprecation/deprecation export interface NavigationButtonProps extends CommonProps { /** The icon. */ iconSpec: IconSpec; diff --git a/src/mobile-ui-react/PanelHeader.tsx b/src/mobile-ui-react/PanelHeader.tsx index 891f5ee..1cb91d3 100644 --- a/src/mobile-ui-react/PanelHeader.tsx +++ b/src/mobile-ui-react/PanelHeader.tsx @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as React from "react"; import classnames from "classnames"; -import { CommonProps } from "@itwin/core-react"; import { withoutClassName } from "@itwin/mobile-sdk-core"; import { DraggableComponent, DraggableComponentCallbackProps } from "./ResizablePanel"; +import { CommonProps } from "./MobileUi"; import "./PanelHeader.scss"; @@ -35,8 +35,6 @@ function PanelHeaderDraggableDiv(props: PanelHeaderDraggableDivProps) { * Properties for the {@link PanelHeaderButton} component. * @public */ -// @todo AppUI deprecation -// eslint-disable-next-line deprecation/deprecation export interface PanelHeaderButtonProps extends CommonProps { /** The button's text label. */ label: string; diff --git a/src/mobile-ui-react/ResizablePanel.tsx b/src/mobile-ui-react/ResizablePanel.tsx index 50eeeaf..5d787e7 100644 --- a/src/mobile-ui-react/ResizablePanel.tsx +++ b/src/mobile-ui-react/ResizablePanel.tsx @@ -5,17 +5,14 @@ import * as React from "react"; import classnames from "classnames"; import { Point2d, XAndY } from "@itwin/core-geometry"; -import { CommonProps } from "@itwin/core-react"; import { getCssVariableAsNumber, ReloadedEvent } from "@itwin/mobile-sdk-core"; -import { ReactUseState, useIsMountedRef, useWindowEvent } from "./MobileUi"; +import { CommonProps, ReactUseState, useIsMountedRef, useWindowEvent } from "./MobileUi"; import "./ResizablePanel.scss"; /** * Properties for {@link ResizablePanel} component * @public */ -// @todo AppUI deprecation -// eslint-disable-next-line deprecation/deprecation export interface ResizablePanelProps extends CommonProps { /** The children */ children?: React.ReactNode; @@ -356,8 +353,6 @@ export function DraggableComponent(props: DraggableComponentProps) { ; } -// @todo AppUI deprecation -// eslint-disable-next-line deprecation/deprecation interface VerticalScrollProps extends CommonProps { children?: React.ReactNode; } @@ -368,8 +363,6 @@ export function VerticalScroll(props: VerticalScrollProps) { return
{children}
; } -// @todo AppUI deprecation -// eslint-disable-next-line deprecation/deprecation interface TouchCaptorProps extends CommonProps { isTouchStarted: boolean; onTouchStart?: (e: TouchEvent) => void; @@ -470,8 +463,6 @@ interface TouchDragHandleState { isPointerDown: boolean; } -// @todo AppUI deprecation -// eslint-disable-next-line deprecation/deprecation interface TouchDragHandleProps extends CommonProps { /** Last pointer position of draggable tab. */ lastPosition?: XAndY; diff --git a/src/mobile-ui-react/ScrollableWithFades.tsx b/src/mobile-ui-react/ScrollableWithFades.tsx index f068d5d..80572cb 100644 --- a/src/mobile-ui-react/ScrollableWithFades.tsx +++ b/src/mobile-ui-react/ScrollableWithFades.tsx @@ -5,8 +5,7 @@ import * as React from "react"; import classnames from "classnames"; import { ColorDef } from "@itwin/core-common"; -import { ClassNameProps } from "@itwin/core-react"; -import { useScroll, useWindowEvent } from "./MobileUi"; +import { ClassNameProps, useScroll, useWindowEvent } from "./MobileUi"; import { getCssVariable } from "@itwin/mobile-sdk-core"; import "./ScrollableWithFades.scss"; @@ -17,8 +16,6 @@ import "./ScrollableWithFades.scss"; * Properties for the {@link HorizontalScrollableWithFades} component. * @public */ -// @todo AppUI deprecation -// eslint-disable-next-line deprecation/deprecation export interface ScrollableWithFadesProps extends ClassNameProps { backgroundColor?: ColorDef; /** The views to go into the scrollable element */ diff --git a/src/mobile-ui-react/Suggestion.tsx b/src/mobile-ui-react/Suggestion.tsx index 4e91a23..dc93a7e 100644 --- a/src/mobile-ui-react/Suggestion.tsx +++ b/src/mobile-ui-react/Suggestion.tsx @@ -5,10 +5,10 @@ import * as React from "react"; import classnames from "classnames"; import { BeUiEvent } from "@itwin/core-bentley"; -import { useOnOutsideClick } from "@itwin/core-react"; import { ToolAssistanceInstructions } from "@itwin/core-frontend"; import { IconImage } from "./IconImage"; import { useBeUiEvent } from "./MobileUi"; +import { useOnOutsideClick } from "./useOnOutsideClick"; import { getCssVariableAsNumber } from "@itwin/mobile-sdk-core"; import "./Suggestion.scss"; @@ -39,8 +39,6 @@ export function SuggestionContainer(props: React.HTMLAttributes) * @public */ export function Suggestion(props: SuggestionProps) { - // @todo AppUI deprecation - // eslint-disable-next-line deprecation/deprecation const ref = useOnOutsideClick(() => props.onOutsideClick?.()); // To ensure the icon is properly centered when the label not displayed, calculate the icon's margin so that its height and width match the pill height. const pillHeight = React.useRef(getCssVariableAsNumber("--mui-pill-height")); diff --git a/src/mobile-ui-react/TileGrid.tsx b/src/mobile-ui-react/TileGrid.tsx index ce6899c..1cdd504 100644 --- a/src/mobile-ui-react/TileGrid.tsx +++ b/src/mobile-ui-react/TileGrid.tsx @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as React from "react"; import classnames from "classnames"; -import { CommonProps } from "@itwin/core-react"; -import { useMediaQuery, useScrolling } from "./MobileUi"; +import { CommonProps, useMediaQuery, useScrolling } from "./MobileUi"; import "./TileGrid.scss"; /** @@ -25,8 +24,6 @@ export interface GridTileSize { * Properties for {@link TileGrid} component. * @public */ -// @todo AppUI deprecation -// eslint-disable-next-line deprecation/deprecation export interface TileGridProps extends CommonProps { /** [[GridTile]] children of this node */ children?: Array>; @@ -55,8 +52,6 @@ export interface GridTileInjectedProps { * Properties for {@link GridTile} component. * @public */ -// @todo AppUI deprecation -// eslint-disable-next-line deprecation/deprecation export interface GridTileProps extends CommonProps { /** onClick handler for this GridTile. */ onClick?: (e: React.MouseEvent) => void; diff --git a/src/mobile-ui-react/Timer.ts b/src/mobile-ui-react/Timer.ts new file mode 100644 index 0000000..8b45b43 --- /dev/null +++ b/src/mobile-ui-react/Timer.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +/* + * NOTE: This entire file was copied from @itwin/core-react, where it is being deprecated. It was + * then modified to make things internal instead of public and deprecated. + */ + +/** Signature for [[Timer]] execute callback. + * @internal + */ +export type ExecuteHandler = (this: void) => void; + +/** Notifies handler after a set interval. + * @internal + */ +export class Timer { + private _delay: number; + private _isRunning = false; + private _timerId = 0; + private _onExecute: ExecuteHandler | undefined; + + /** + * Creates a new Timer. + * @param msDelay Time interval in milliseconds after which handler will be notified. + */ + public constructor(msDelay: number) { + this._delay = msDelay; + } + + /** Indicates whether the timer is running */ + public get isRunning(): boolean { + return this._isRunning; + } + + /** Time interval in milliseconds after which handler will be notified. */ + public get delay() { + return this._delay; + } + public set delay(ms: number) { + this._delay = ms; + } + + /** Set handler that is called after a set interval. */ + public setOnExecute(onExecute: ExecuteHandler | undefined) { + this._onExecute = onExecute; + } + + /** Starts this Timer. */ + public start() { + if (this._isRunning) this.clearTimeout(); + + this._isRunning = true; + this.setTimeout(); + } + + /** Stops this Timer. */ + public stop() { + if (!this._isRunning) return; + + this._isRunning = false; + this.clearTimeout(); + } + + private execute() { + this._onExecute && this._onExecute(); + this._isRunning = false; + } + + private setTimeout() { + this._timerId = window.setTimeout(() => this.execute(), this._delay); + } + + private clearTimeout() { + window.clearTimeout(this._timerId); + } +} diff --git a/src/mobile-ui-react/useOnOutsideClick.tsx b/src/mobile-ui-react/useOnOutsideClick.tsx new file mode 100644 index 0000000..c19cd85 --- /dev/null +++ b/src/mobile-ui-react/useOnOutsideClick.tsx @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +/* + * NOTE: This entire file was copied from @itwin/core-react, where it is being deprecated. It was + * then modified to make things internal instead of public and deprecated. + */ + +import * as React from "react"; +import { Timer } from "./Timer"; + +function hasPointerEventsSupport() { + return !!window.PointerEvent; +} + +/** + * @internal + */ +export type OutsideClickEvent = PointerEvent | MouseEvent | TouchEvent; + +/** Invokes onOutsideClick handler when user clicks outside of referenced element. + * @internal + */ +export function useOnOutsideClick( + onOutsideClick?: () => void, + /** Invoked for intermediate events. Return `false` to prevent outside click. */ + outsideEventPredicate?: (e: OutsideClickEvent) => boolean, +) { + const handleMouseEvents = React.useRef(true); + const handleMouseEventsTimer = React.useRef(new Timer(1000)); + const ref = React.useRef(null); + const isDownOutside = React.useRef(false); + React.useEffect(() => { + const listener = (e: OutsideClickEvent) => { + if (e.type === "touchstart") { + // Skip mouse event handlers after touch event. + handleMouseEvents.current = false; + handleMouseEventsTimer.current.start(); + } else if (e.type === "mousedown" && !handleMouseEvents.current) { + return; + } + const isOutsideEvent = !outsideEventPredicate || outsideEventPredicate(e); + isDownOutside.current = + !!ref.current && + e.target instanceof Node && + !ref.current.contains(e.target) && + isOutsideEvent; + }; + if (hasPointerEventsSupport()) { + document.addEventListener("pointerdown", listener); + } else { + document.addEventListener("mousedown", listener); + document.addEventListener("touchstart", listener); + } + return () => { + if (hasPointerEventsSupport()) { + document.removeEventListener("pointerdown", listener); + } else { + document.removeEventListener("mousedown", listener); + document.removeEventListener("touchstart", listener); + } + }; + }, [outsideEventPredicate]); + React.useEffect(() => { + const listener = (e: OutsideClickEvent) => { + if (e.type === "mouseup" && !handleMouseEvents.current) { + return; + } + onOutsideClick && + isDownOutside.current && + (!outsideEventPredicate || outsideEventPredicate(e)) && + ref.current && + e.target instanceof Node && + !ref.current.contains(e.target) && + onOutsideClick(); + isDownOutside.current = false; + }; + if (hasPointerEventsSupport()) { + document.addEventListener("pointerup", listener); + } else { + document.addEventListener("mouseup", listener); + document.addEventListener("touchend", listener); + } + return () => { + if (hasPointerEventsSupport()) { + document.removeEventListener("pointerup", listener); + } else { + document.removeEventListener("mouseup", listener); + document.removeEventListener("touchend", listener); + } + }; + }, [onOutsideClick, outsideEventPredicate]); + React.useEffect(() => { + const listener = () => { + handleMouseEvents.current = true; + }; + const timer = handleMouseEventsTimer.current; + timer.setOnExecute(listener); + return () => { + timer.setOnExecute(undefined); + }; + }, []); + return ref; +}