Skip to content

Commit

Permalink
feat: zoom in out (microsoft#4217)
Browse files Browse the repository at this point in the history
* zoom in out

* add removeEventListener

* add zoom into recoil

* button

* ux adjust

* 1. flash when click button 2, add home button 3, set min zoom out to 50% 4, add disabled state

* disable style

* transform position not move

* rename function name & fix some bug

* zoom scroll

* scroll to top

Co-authored-by: Andy Brown <[email protected]>
Co-authored-by: Chris Whitten <[email protected]>
Co-authored-by: zeye <[email protected]>
  • Loading branch information
4 people authored Oct 14, 2020
1 parent e000405 commit 6509433
Show file tree
Hide file tree
Showing 9 changed files with 250 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
VisualEditorElementWrapper,
} from './renderers';
import { useFlowUIOptions } from './hooks/useFlowUIOptions';
import { ZoomZone } from './components/ZoomZone';

formatMessage.setup({
missingTranslation: 'ignore',
Expand All @@ -46,8 +47,6 @@ const styles = css`
left: 0;
right: 0;
overflow: scroll;
border: 1px solid transparent;
&:focus {
Expand All @@ -73,8 +72,11 @@ const VisualDesigner: React.FC<VisualDesignerProps> = ({ onFocus, onBlur, schema
data: inputData,
hosted,
schemas,
flowZoomRate,
} = shellData;

const { updateFlowZoomRate } = shellApi;

const dataCache = useRef({});

/**
Expand Down Expand Up @@ -116,7 +118,6 @@ const VisualDesigner: React.FC<VisualDesignerProps> = ({ onFocus, onBlur, schema
}, {} as FlowUISchema);

const divRef = useRef<HTMLDivElement>(null);

// send focus to the keyboard area when navigating to a new trigger
useEffect(() => {
divRef.current?.focus();
Expand All @@ -143,42 +144,44 @@ const VisualDesigner: React.FC<VisualDesignerProps> = ({ onFocus, onBlur, schema
{...enableKeyboardCommandAttributes(handleCommand)}
data-testid="visualdesigner-container"
>
<SelectionContext.Provider value={selectionContext}>
<MarqueeSelection css={{ width: '100%', height: '100%' }} selection={selection}>
<div
className="flow-editor-container"
css={{
width: '100%',
height: '100%',
padding: '48px 20px',
boxSizing: 'border-box',
}}
data-testid="flow-editor-container"
onClick={(e) => {
e.stopPropagation();
handleEditorEvent(NodeEventTypes.Focus, { id: '' });
}}
>
<AdaptiveDialog
activeTrigger={focusedEvent}
dialogData={data}
dialogId={dialogId}
renderers={{
EdgeMenu: VisualEditorEdgeMenu,
NodeMenu: VisualEditorNodeMenu,
NodeWrapper: VisualEditorNodeWrapper,
ElementWrapper: VisualEditorElementWrapper,
<ZoomZone flowZoomRate={flowZoomRate} focusedId={focusedId} updateFlowZoomRate={updateFlowZoomRate}>
<SelectionContext.Provider value={selectionContext}>
<MarqueeSelection selection={selection}>
<div
className="flow-editor-container"
css={{
width: '100%',
height: '100%',
padding: '48px 20px',
boxSizing: 'border-box',
}}
schema={{ ...schemaFromPlugins, ...customFlowSchema }}
widgets={widgetsFromPlugins}
onEvent={(eventName, eventData) => {
divRef.current?.focus({ preventScroll: true });
handleEditorEvent(eventName, eventData);
data-testid="flow-editor-container"
onClick={(e) => {
e.stopPropagation();
handleEditorEvent(NodeEventTypes.Focus, { id: '' });
}}
/>
</div>
</MarqueeSelection>
</SelectionContext.Provider>
>
<AdaptiveDialog
activeTrigger={focusedEvent}
dialogData={data}
dialogId={dialogId}
renderers={{
EdgeMenu: VisualEditorEdgeMenu,
NodeMenu: VisualEditorNodeMenu,
NodeWrapper: VisualEditorNodeWrapper,
ElementWrapper: VisualEditorElementWrapper,
}}
schema={{ ...schemaFromPlugins, ...customFlowSchema }}
widgets={widgetsFromPlugins}
onEvent={(eventName, eventData) => {
divRef.current?.focus({ preventScroll: true });
handleEditorEvent(eventName, eventData);
}}
/>
</div>
</MarqueeSelection>
</SelectionContext.Provider>
</ZoomZone>
</div>
</SelfHostContext.Provider>
</NodeRendererContext.Provider>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/** @jsx jsx */
import { jsx, css } from '@emotion/core';
import { useRef, useEffect, ReactNode } from 'react';
import { ZoomInfo } from '@bfc/shared';
import { IconButton, IButtonStyles } from 'office-ui-fabric-react/lib/Button';
import { IIconProps } from 'office-ui-fabric-react/lib/Icon';

import { scrollNodeIntoView } from '../utils/scrollNodeIntoView';
import { AttrNames } from '../constants/ElementAttributes';

function scrollZoom(delta: number, rateList: number[], maxRate: number, minRate: number, currentRate: number): number {
let rate: number = currentRate;

if (delta < 0) {
// Zoom in
rate = rateList[rateList.indexOf(currentRate) + 1] || rate;
rate = Math.min(maxRate, rate);
} else if (delta > 0) {
// Zoom out
rate = rateList[rateList.indexOf(currentRate) - 1] || rate;
rate = Math.max(minRate, rate);
} else {
rate = 1;
}

return rate;
}

interface ZoomZoneProps {
flowZoomRate: ZoomInfo;
focusedId: string;
updateFlowZoomRate: (currentRate: number) => void;
children?: ReactNode;
}

export const ZoomZone: React.FC<ZoomZoneProps> = ({ flowZoomRate, focusedId, updateFlowZoomRate, children }) => {
const divRef = useRef<HTMLDivElement>(null);
const { rateList, maxRate, minRate, currentRate } = flowZoomRate || {
rateList: [0.5, 1, 3],
maxRate: 3,
minRate: 0.5,
currentRate: 1,
};
const onWheel = (event: WheelEvent) => {
if (event.ctrlKey) {
event.preventDefault();
event.stopPropagation();
handleZoom(event.deltaY);
}
};

const handleZoom = (delta: number) => {
const rate = scrollZoom(delta, rateList, maxRate, minRate, currentRate);

updateFlowZoomRate(rate);
};

const container = divRef.current as HTMLElement;
useEffect(() => {
if (!container) return;
const target = container.children[0] as HTMLElement;
target.style.transform = `scale(${currentRate})`;
target.style.transformOrigin = 'top left';
container.scroll({
top: (container.scrollWidth - container.clientWidth) / 2,
left: (container.scrollHeight - container.clientHeight) / 2,
});

if (currentRate === 1) {
scrollNodeIntoView(`[${AttrNames.SelectedId}="${focusedId}"]`);
}
}, [currentRate]);

const buttonRender = () => {
const buttonBoxStyle = css({ position: 'absolute', left: '25px', bottom: '25px', width: '35px' });
const iconStyle = (zoom: string): IIconProps => {
return zoom === 'in'
? { iconName: 'ZoomIn', styles: { root: { color: '#fff' } } }
: { iconName: 'ZoomOut', styles: { root: { color: '#fff' } } };
};
const buttonStyle: IButtonStyles = {
root: {
width: '35px',
height: '35px',
background: 'rgba(44, 41, 41, 0.8)',
borderRadius: '2px',
margin: '2.5px 0',
selectors: {
':disabled': {
backgroundColor: '#BDBDBD',
},
},
},
rootHovered: {
backgroundColor: 'rgba(44, 41, 41, 0.8)',
},
rootPressed: {
backgroundColor: 'rgba(44, 41, 41, 0.8)',
},
};
return (
<div css={buttonBoxStyle}>
<IconButton
disabled={currentRate === maxRate}
iconProps={iconStyle('in')}
styles={buttonStyle}
onClick={() => handleZoom(-100)}
></IconButton>
<IconButton
disabled={currentRate === minRate}
iconProps={iconStyle('out')}
styles={buttonStyle}
onClick={() => handleZoom(100)}
></IconButton>
<IconButton
styles={buttonStyle}
onClick={() => {
handleZoom(0);
container.scrollTo({ top: 0 });
}}
>
<svg fill="none" height="15" viewBox="0 0 15 15" width="15" xmlns="http://www.w3.org/2000/svg">
<path
d="M7.5 5.5C7.77604 5.5 8.03385 5.55208 8.27344 5.65625C8.51823 5.76042 8.73177 5.90365 8.91406 6.08594C9.09635 6.26823 9.23958 6.48177 9.34375 6.72656C9.44792 6.96615 9.5 7.22396 9.5 7.5C9.5 7.77604 9.44792 8.03646 9.34375 8.28125C9.23958 8.52083 9.09635 8.73177 8.91406 8.91406C8.73177 9.09635 8.51823 9.23958 8.27344 9.34375C8.03385 9.44792 7.77604 9.5 7.5 9.5C7.22396 9.5 6.96354 9.44792 6.71875 9.34375C6.47917 9.23958 6.26823 9.09635 6.08594 8.91406C5.90365 8.73177 5.76042 8.52083 5.65625 8.28125C5.55208 8.03646 5.5 7.77604 5.5 7.5C5.5 7.22396 5.55208 6.96615 5.65625 6.72656C5.76042 6.48177 5.90365 6.26823 6.08594 6.08594C6.26823 5.90365 6.47917 5.76042 6.71875 5.65625C6.96354 5.55208 7.22396 5.5 7.5 5.5ZM15 8H13.4766C13.4401 8.47917 13.3464 8.94531 13.1953 9.39844C13.0495 9.84635 12.8516 10.2682 12.6016 10.6641C12.3568 11.0547 12.0703 11.4141 11.7422 11.7422C11.4141 12.0703 11.0521 12.3594 10.6562 12.6094C10.2656 12.8542 9.84375 13.0521 9.39062 13.2031C8.94271 13.349 8.47917 13.4401 8 13.4766V15H7V13.4766C6.52083 13.4401 6.05469 13.349 5.60156 13.2031C5.15365 13.0521 4.73177 12.8542 4.33594 12.6094C3.94531 12.3594 3.58594 12.0703 3.25781 11.7422C2.92969 11.4141 2.64062 11.0547 2.39062 10.6641C2.14583 10.2682 1.94792 9.84635 1.79688 9.39844C1.65104 8.95052 1.5599 8.48438 1.52344 8H0V7H1.52344C1.5599 6.52083 1.65104 6.05729 1.79688 5.60938C1.94792 5.15625 2.14583 4.73438 2.39062 4.34375C2.64062 3.94792 2.92969 3.58594 3.25781 3.25781C3.58594 2.92969 3.94531 2.64323 4.33594 2.39844C4.73177 2.14844 5.15365 1.95052 5.60156 1.80469C6.04948 1.65365 6.51562 1.5599 7 1.52344V0H8V1.52344C8.48438 1.5599 8.95052 1.65365 9.39844 1.80469C9.84635 1.95052 10.2656 2.14844 10.6562 2.39844C11.0521 2.64323 11.4141 2.92969 11.7422 3.25781C12.0703 3.58594 12.3568 3.94792 12.6016 4.34375C12.8516 4.73438 13.0495 5.15625 13.1953 5.60938C13.3464 6.05729 13.4401 6.52083 13.4766 7H15V8ZM7.5 12.5C7.95833 12.5 8.40104 12.4401 8.82812 12.3203C9.25521 12.2005 9.65365 12.0339 10.0234 11.8203C10.3932 11.6016 10.7292 11.3411 11.0312 11.0391C11.3385 10.7318 11.599 10.3932 11.8125 10.0234C12.0312 9.65365 12.2005 9.25521 12.3203 8.82812C12.4401 8.40104 12.5 7.95833 12.5 7.5C12.5 7.04167 12.4401 6.59896 12.3203 6.17188C12.2005 5.74479 12.0312 5.34635 11.8125 4.97656C11.599 4.60677 11.3385 4.27083 11.0312 3.96875C10.7292 3.66146 10.3932 3.40104 10.0234 3.1875C9.65365 2.96875 9.25521 2.79948 8.82812 2.67969C8.40104 2.5599 7.95833 2.5 7.5 2.5C7.04167 2.5 6.59896 2.5599 6.17188 2.67969C5.74479 2.79948 5.34635 2.96875 4.97656 3.1875C4.60677 3.40104 4.26823 3.66146 3.96094 3.96875C3.65885 4.27083 3.39844 4.60677 3.17969 4.97656C2.96615 5.34635 2.79948 5.74479 2.67969 6.17188C2.5599 6.59896 2.5 7.04167 2.5 7.5C2.5 7.95833 2.5599 8.40104 2.67969 8.82812C2.79948 9.25521 2.96615 9.65365 3.17969 10.0234C3.39844 10.3932 3.65885 10.7318 3.96094 11.0391C4.26823 11.3411 4.60677 11.6016 4.97656 11.8203C5.34635 12.0339 5.74479 12.2005 6.17188 12.3203C6.59896 12.4401 7.04167 12.5 7.5 12.5Z"
fill="white"
/>
</svg>
</IconButton>
</div>
);
};

// Using ref and eventListener instead of <div @wheel='xxx()' /> because passive property can not be set in <div @wheel='xxx()' />
useEffect(() => {
if (flowZoomRate) {
divRef.current?.addEventListener('wheel', onWheel, { passive: false });
}
return () => divRef.current?.removeEventListener('wheel', onWheel);
}, [flowZoomRate]);

return (
<div ref={divRef} css={{ overflow: 'scroll', width: '100%', height: '100%' }}>
{children}
{buttonRender()}
</div>
);
};
2 changes: 2 additions & 0 deletions Composer/packages/client/src/pages/design/DesignPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ const DesignPage: React.FC<RouteComponentProps<{ dialogId: string; projectId: st
onboardingAddCoachMarkRef,
importQnAFromUrls,
addSkill,
updateZoomRate,
} = useRecoilValue(dispatcherState);

const params = new URLSearchParams(location?.search);
Expand Down Expand Up @@ -249,6 +250,7 @@ const DesignPage: React.FC<RouteComponentProps<{ dialogId: string; projectId: st
};

function handleSelect(projectId, id, selected = '') {
updateZoomRate({ currentRate: 1 });
if (selected) {
selectTo(projectId, selected);
} else {
Expand Down
1 change: 1 addition & 0 deletions Composer/packages/client/src/recoilModel/atoms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@

export * from './appState';
export * from './botState';
export * from './zoomState';
19 changes: 19 additions & 0 deletions Composer/packages/client/src/recoilModel/atoms/zoomState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { atom } from 'recoil';
import { ZoomInfo } from '@bfc/shared';

const getFullyQualifiedKey = (value: string) => {
return `Zoom_${value}_State`;
};

export const rateInfoState = atom<ZoomInfo>({
key: getFullyQualifiedKey('rateInfo'),
default: {
rateList: [0.25, 0.33, 0.5, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.75, 2, 2.5, 3, 4, 5],
maxRate: 3,
minRate: 0.5,
currentRate: 1,
},
});
2 changes: 2 additions & 0 deletions Composer/packages/client/src/recoilModel/dispatchers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { multilangDispatcher } from './multilang';
import { notificationDispatcher } from './notification';
import { extensionsDispatcher } from './extensions';
import { botProjectFileDispatcher } from './botProjectFile';
import { zoomDispatcher } from './zoom';

const createDispatchers = () => {
return {
Expand All @@ -44,6 +45,7 @@ const createDispatchers = () => {
...notificationDispatcher(),
...extensionsDispatcher(),
...botProjectFileDispatcher(),
...zoomDispatcher(),
};
};

Expand Down
18 changes: 18 additions & 0 deletions Composer/packages/client/src/recoilModel/dispatchers/zoom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* eslint-disable react-hooks/rules-of-hooks */
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { CallbackInterface, useRecoilCallback } from 'recoil';

import { rateInfoState } from '../atoms/zoomState';

export const zoomDispatcher = () => {
const updateZoomRate = useRecoilCallback(({ set }: CallbackInterface) => async ({ currentRate }) => {
set(rateInfoState, (rateInfo) => {
return { ...rateInfo, currentRate };
});
});
return {
updateZoomRate,
};
};
9 changes: 9 additions & 0 deletions Composer/packages/client/src/shell/useShell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
dialogSchemasState,
lgFilesState,
luFilesState,
rateInfoState,
} from '../recoilModel';
import { undoFunctionState } from '../recoilModel/undo/history';

Expand Down Expand Up @@ -56,6 +57,7 @@ export function useShell(source: EventSource, projectId: string): Shell {
const dialogSchemas = useRecoilValue(dialogSchemasState(projectId));
const botName = useRecoilValue(botDisplayNameState(projectId));
const settings = useRecoilValue(settingsState(projectId));
const flowZoomRate = useRecoilValue(rateInfoState);

const userSettings = useRecoilValue(userSettingsState);
const clipboardActions = useRecoilValue(clipboardActionsState);
Expand All @@ -74,6 +76,7 @@ export function useShell(source: EventSource, projectId: string): Shell {
setMessage,
displayManifestModal,
updateSkill,
updateZoomRate,
} = useRecoilValue(dispatcherState);

const lgApi = useLgApi(projectId);
Expand Down Expand Up @@ -133,6 +136,10 @@ export function useShell(source: EventSource, projectId: string): Shell {
focusTo(projectId, dataPath, fragment ?? '');
}

function updateFlowZoomRate(currentRate) {
updateZoomRate({ currentRate });
}

dialogMapRef.current = dialogsMap;

const api: ShellApi = {
Expand Down Expand Up @@ -204,6 +211,7 @@ export function useShell(source: EventSource, projectId: string): Shell {
updateDialogSchema(dialogSchema, projectId);
},
updateSkillSetting: (...params) => updateSkill(projectId, ...params),
updateFlowZoomRate,
};

const currentDialog = useMemo(() => dialogs.find((d) => d.id === dialogId), [dialogs, dialogId]);
Expand Down Expand Up @@ -239,6 +247,7 @@ export function useShell(source: EventSource, projectId: string): Shell {
luFeatures: settings.luFeatures,
skills,
skillsSettings: settings.skill || {},
flowZoomRate,
}
: ({
projectId,
Expand Down
Loading

0 comments on commit 6509433

Please sign in to comment.