diff --git a/Composer/packages/client/__tests__/mocks/ResizeObserver.ts b/Composer/packages/client/__tests__/mocks/ResizeObserver.ts new file mode 100644 index 0000000000..81607f4b4a --- /dev/null +++ b/Composer/packages/client/__tests__/mocks/ResizeObserver.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export default class ResizeObserver { + observe() { + // do nothing + } + unobserve() { + // do nothing + } + disconnect() { + // do nothing + } +} diff --git a/Composer/packages/client/__tests__/pages/design/Design.test.tsx b/Composer/packages/client/__tests__/pages/design/Design.test.tsx index d3b12856af..507ca29aa9 100644 --- a/Composer/packages/client/__tests__/pages/design/Design.test.tsx +++ b/Composer/packages/client/__tests__/pages/design/Design.test.tsx @@ -16,6 +16,9 @@ import { undoFunctionState } from '../../../src/recoilModel/undo/history'; import mockProjectResponse from '../../../src/recoilModel/dispatchers/__tests__/mocks/mockProjectResponse.json'; import DesignPage from '../../../src/pages/design/DesignPage'; import { SAMPLE_DIALOG } from '../../mocks/sampleDialog'; +import ResizeObserver from '../../mocks/ResizeObserver'; + +(global as any).ResizeObserver = ResizeObserver; const projectId = '12345.6789'; const dialogId = SAMPLE_DIALOG.id; diff --git a/Composer/packages/client/src/hooks/useResizeObserver.ts b/Composer/packages/client/src/hooks/useResizeObserver.ts new file mode 100644 index 0000000000..6357cdebd8 --- /dev/null +++ b/Composer/packages/client/src/hooks/useResizeObserver.ts @@ -0,0 +1,39 @@ +/* eslint-disable @typescript-eslint/ban-ts-ignore */ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import React from 'react'; + +/* + * TODO: remove ts-ignore after updating typescript to v4. + * current typescript version does not have definitions for ResizeObserver. + */ + +/** + * Observes size changes of a given element and runs the provided callback. + * @param element Element to observe size changes. + * @param resizeCallback Callback to call when resize happens. + * @param options Optional options for resize observer. + */ +export const useResizeObserver = ( + element: T | null, + // @ts-ignore + resizeCallback: (entries: ResizeObserverEntry[]) => void, + // @ts-ignore + options?: ResizeObserverOptions +) => { + // @ts-ignore + const resizeObserver = React.useRef(new ResizeObserver(resizeCallback)); + + React.useEffect(() => { + if (element) { + resizeObserver.current.observe(element, options); + } + + return () => { + if (element) { + resizeObserver.current.unobserve(element); + } + }; + }, [element]); +}; diff --git a/Composer/packages/client/src/pages/design/VisualPanelHeader.tsx b/Composer/packages/client/src/pages/design/VisualPanelHeader.tsx index 8cb60601e4..951ae1f1b2 100644 --- a/Composer/packages/client/src/pages/design/VisualPanelHeader.tsx +++ b/Composer/packages/client/src/pages/design/VisualPanelHeader.tsx @@ -17,8 +17,9 @@ import { getDialogData } from '../../utils/dialogUtil'; import { decodeDesignerPathToArrayPath } from '../../utils/convertUtils/designerPathEncoder'; import { getFocusPath } from '../../utils/navigation'; import { TreeLink } from '../../components/ProjectTree/types'; +import { useResizeObserver } from '../../hooks/useResizeObserver'; -import { breadcrumbClass } from './styles'; +import * as pageStyles from './styles'; type BreadcrumbItem = { key: string; @@ -155,11 +156,40 @@ const useBreadcrumbs = (projectId: string, pluginConfig?: PluginConfig) => { }, [currentDialog?.content, initialBreadcrumbArray]); return breadcrumbArray; }; - +const defaultToggleButtonWidth = 100; +const spaceBetweenContainers = 6; const VisualPanelHeader: React.FC = React.memo((props) => { const { showCode, projectId, onShowCodeClick, pluginConfig } = props; + const breadcrumbs = useBreadcrumbs(projectId, pluginConfig); + const toggleButtonContainerRef = React.useRef(null); + const containerRef = React.useRef(null); + + const [toggleButtonWidth, setToggleButtonWidth] = React.useState(defaultToggleButtonWidth); + const [breadcrumbContainerWidth, setBreadcrumbContainerWidth] = React.useState( + `calc(100% - ${toggleButtonWidth}px)` + ); + + // Set the width of the toggle button based on its text (locale) + React.useEffect(() => { + if (toggleButtonContainerRef.current) { + const toggleButton = toggleButtonContainerRef.current.querySelector('button'); + if (toggleButton) { + const { width } = toggleButton?.getBoundingClientRect(); + setToggleButtonWidth(width); + } + } + }, []); + + // Observe width changes of the container to re-set the available width for breadcrumb container + useResizeObserver(containerRef.current, (entries) => { + if (entries.length) { + const { width } = entries[0].contentRect; + setBreadcrumbContainerWidth(width - toggleButtonWidth - spaceBetweenContainers); + } + }); + const createBreadcrumbItem: (breadcrumb: BreadcrumbItem) => IBreadcrumbItem = (breadcrumb: BreadcrumbItem) => { return { key: breadcrumb.key, @@ -171,16 +201,16 @@ const VisualPanelHeader: React.FC = React.memo((props) = const items = breadcrumbs.map(createBreadcrumbItem); return ( -
- undefined} - /> -
+
+
+ +
+
{showCode ? formatMessage('Hide code') : formatMessage('Show code')} diff --git a/Composer/packages/client/src/pages/design/styles.ts b/Composer/packages/client/src/pages/design/styles.ts index 9d0e9a70be..cfe8679dee 100644 --- a/Composer/packages/client/src/pages/design/styles.ts +++ b/Composer/packages/client/src/pages/design/styles.ts @@ -103,9 +103,19 @@ export const formEditor = css` min-width: 300px; `; +export const visualPanelHeaderContainer = css` + display: flex; + align-items: center; + height: 65px; +`; + +export const visualPanelHeaderShowCodeButton = css` + padding: 10px; + width: fit-content; +`; + export const breadcrumbClass = mergeStyleSets({ root: { - width: '500px', margin: '0', padding: '10px', },