diff --git a/src/BloomBrowserUI/bookEdit/js/canvasElementManager/CanvasElementContextControls.tsx b/src/BloomBrowserUI/bookEdit/js/canvasElementManager/CanvasElementContextControls.tsx index 0ff4d839fb98..8ddc7d813f2d 100644 --- a/src/BloomBrowserUI/bookEdit/js/canvasElementManager/CanvasElementContextControls.tsx +++ b/src/BloomBrowserUI/bookEdit/js/canvasElementManager/CanvasElementContextControls.tsx @@ -7,14 +7,7 @@ import { kBloomBlue, lightTheme } from "../../../bloomMaterialUITheme"; import { SvgIconProps } from "@mui/material"; import { default as MenuIcon } from "@mui/icons-material/MoreHorizSharp"; import { ThemeProvider } from "@mui/material/styles"; -import { - divider, - ILocalizableMenuItemProps, - LocalizableMenuItem, - LocalizableNestedMenuItem, -} from "../../../react_components/localizableMenuItem"; import Menu from "@mui/material/Menu"; -import { Divider } from "@mui/material"; import { getCanvasElementManager } from "../../toolbox/canvas/canvasElementPageBridge"; import { kBackgroundImageClass } from "../../toolbox/canvas/canvasElementConstants"; import { BloomTooltip } from "../../../react_components/BloomToolTip"; @@ -27,6 +20,7 @@ import { getAudioSentencesOfVisibleEditables } from "bloom-player"; import { canvasElementControlRegistry } from "../../toolbox/canvas/canvasElementControlRegistry"; import { buildCanvasElementControlRegistryContext } from "../../toolbox/canvas/buildCanvasElementControlRegistryContext"; import { + ICanvasElementControlConfiguration, IControlContext, ILanguageNameValues, IControlMenuRow, @@ -36,17 +30,46 @@ import { getMenuSections, getToolbarItems, } from "../../toolbox/canvas/canvasControlResolution"; - -interface IMenuItemWithSubmenu extends ILocalizableMenuItemProps { - subMenu?: ILocalizableMenuItemProps[]; -} +import { + contextMenuCss, + convertControlMenuRows, + IMenuItemWithSubmenu, + joinMenuSectionsWithSingleDividers, + renderContextMenuItems, + runControlCallback, + scaleIconNode, +} from "./canvasControlMenuRendering"; // This is the controls bar that appears beneath a canvas element when it is selected. It contains buttons // for the most common operations that apply to the canvas element in its current state, and a menu for less common // operations. -const CanvasElementContextControls: React.FunctionComponent<{ +/** + * What a caller must supply to put this bar on something that is not a canvas element. An + * inline image -- a picture inside a text block -- passes this, so that a picture offers the + * same toolbar wherever the user meets one (see inlineImageInteractions.ts). + */ +export interface IControlsForNonCanvasObject { + // Which controls the bar and the menu offer, in place of the canvas element registry's + // entry for the element's type. + configuration: ICanvasElementControlConfiguration; + // The menu behind the "..." button, already built. + menuItems: IMenuItemWithSubmenu[]; + // Merged over the context the registry commands run in. This is where an object says + // how to delete itself (IControlContext.deleteThisObject). + contextAdditions: Partial; + // Run after a toolbar command's action, for the buttons this component builds itself. A + // control that supplies its own toolbar.render owns its interactions and does not go + // through here -- nothing in the inline-image toolbar does, but a control added there + // later would need to call this itself. An inline image uses it to stamp what the command + // did onto the copies in the other languages. + afterToolbarCommand?: () => void; +} + +export const CanvasElementContextControls: React.FunctionComponent<{ canvasElement: HTMLElement; + // Left out for a canvas element, which is what this bar was written for. + controlsForNonCanvasObject?: IControlsForNonCanvasObject; // These props support reusing the context controls menu for a right-click on the canvas element. // The first two make the open state of the menu a controlled property. Basically the // parent stores the state and passes it in, but to get the normal behavior of @@ -225,11 +248,13 @@ const CanvasElementContextControls: React.FunctionComponent<{ onClick: () => void; iconScale?: number; disabled?: boolean; + testId?: string; }): IToolbarItem => { return { key: props.key, node: ( void | Promise, - ): void => { - try { - const result = callback(); - if (result) { - void result.catch((error) => { - console.error( - `Canvas control callback failed (${callbackLabel})`, - error, - ); - }); - } - } catch (error) { - console.error( - `Canvas control callback failed (${callbackLabel})`, - error, - ); - } - }; - const getSpacerToolbarItem = (index: number): IToolbarItem => { return { key: `spacer-${index}`, @@ -297,76 +295,6 @@ const CanvasElementContextControls: React.FunctionComponent<{ let toolbarItems: IToolbarItem[] = []; - const convertControlMenuRows = ( - rows: IControlMenuRow[], - controlContext: IControlContext, - controlRuntime: IControlRuntime, - ): IMenuItemWithSubmenu[] => { - const convertedRows: IMenuItemWithSubmenu[] = []; - - rows.forEach((row) => { - if (row.separatorAbove && convertedRows.length > 0) { - convertedRows.push(divider as IMenuItemWithSubmenu); - } - - const convertedSubMenu = row.subMenuItems - ? convertControlMenuRows( - row.subMenuItems, - controlContext, - controlRuntime, - ) - : undefined; - - const convertedRow: IMenuItemWithSubmenu = { - l10nId: row.l10nId ?? null, - english: row.englishLabel ?? "", - subLabelL10nId: row.subLabelL10nId, - generatedSubLabel: row.subLabel, - shortcutDisplay: row.shortcut?.display, - icon: scaleIconNode(row.icon, row.iconScale), - disabled: row.disabled, - featureName: row.featureName, - subscriptionTooltipOverride: row.subscriptionTooltipOverride, - onClick: () => { - // Ordinary leaf commands close centrally here. Registry - // handlers only call runtime.closeMenu(...) for special - // cases such as dialog launches or submenu-specific focus - // behavior. - if (!convertedSubMenu) { - controlRuntime.closeMenu(); - } - runControlCallback( - `menu:${row.id ?? row.englishLabel ?? "unknown"}`, - () => row.onSelect(controlContext, controlRuntime), - ); - }, - }; - - if (convertedSubMenu) { - convertedRow.subMenu = convertedSubMenu; - } - - convertedRows.push(convertedRow); - - if (row.helpRowL10nId || row.helpRowEnglish) { - if (row.helpRowSeparatorAbove && convertedRows.length > 0) { - convertedRows.push(divider as IMenuItemWithSubmenu); - } - - convertedRows.push({ - l10nId: null, - english: "", - subLabelL10nId: row.helpRowL10nId, - generatedSubLabel: row.helpRowEnglish, - onClick: () => {}, - disabled: true, - }); - } - }); - - return convertedRows; - }; - const getToolbarItemForResolvedControl = ( item: ReturnType[number], index: number, @@ -394,11 +322,12 @@ const CanvasElementContextControls: React.FunctionComponent<{ const icon = control.toolbar?.icon ?? control.icon; const iconScale = control.toolbar?.iconScale ?? control.iconScale; const onClick = () => { - runControlCallback(`toolbar:${control.id}`, () => - control.action(controlContext, { + runControlCallback(`toolbar:${control.id}`, async () => { + await control.action(controlContext, { closeMenu: () => {}, - }), - ); + }); + props.controlsForNonCanvasObject?.afterToolbarCommand?.(); + }); }; if (typeof icon === "function") { @@ -409,6 +338,7 @@ const CanvasElementContextControls: React.FunctionComponent<{ onClick, iconScale: iconScale ?? 1, disabled: !item.enabled, + testId: `toolbar-${control.id}`, }); } @@ -432,6 +362,7 @@ const CanvasElementContextControls: React.FunctionComponent<{ }} >