Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions Composer/packages/adaptive-form/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@
},
"license": "MIT",
"peerDependencies": {
"@bfc/code-editor": "*",
"@bfc/extension-client": "*",
"@bfc/intellisense": "*",
"@uifabric/fluent-theme": "^7.1.4",
"@uifabric/icons": "^7.3.0",
"@uifabric/styling": "^7.7.4",
Expand All @@ -30,20 +27,17 @@
"react-dom": "16.13.1"
},
"devDependencies": {
"@bfc/code-editor": "*",
"@bfc/extension-client": "*",
"@bfc/intellisense": "*",
"@botframework-composer/test-utils": "*",
"@types/lodash": "^4.14.149",
"@types/react": "16.9.23",
"format-message": "^6.2.3",
"react": "16.13.1",
"react-dom": "16.13.1"
"@types/react": "16.9.23"
},
"dependencies": {
"@bfc/built-in-functions": "*",
"@bfc/code-editor": "*",
"@bfc/extension-client": "*",
"@bfc/intellisense": "*",
"@emotion/core": "^10.0.27",
"lodash": "^4.17.19",
"react-error-boundary": "^1.2.5",
"@bfc/built-in-functions": "*"
"react-error-boundary": "^1.2.5"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { DirectionalHint } from 'office-ui-fabric-react/lib/ContextualMenu';
import React from 'react';
import { Callout } from 'office-ui-fabric-react/lib/Callout';
import { FieldToolbar } from '@bfc/code-editor';
import { useShellApi } from '@bfc/extension-client';

const inputs = ['input', 'textarea'];

type Props = {
container: HTMLDivElement | null;
target: HTMLInputElement | HTMLTextAreaElement | null;
value?: string;
onChange: (expression: string) => void;
onClearTarget: () => void;
};

const jsFieldToolbarMenuClassName = 'js-field-toolbar-menu';

export const ExpressionFieldToolbar = (props: Props) => {
const { onClearTarget, container, target, value = '', onChange } = props;
const { projectId, shellApi } = useShellApi();

const [memoryVariables, setMemoryVariables] = React.useState<string[] | undefined>();

React.useEffect(() => {
const abortController = new AbortController();
(async () => {
try {
const variables = await shellApi.getMemoryVariables(projectId, { signal: abortController.signal });
setMemoryVariables(variables);
} catch (e) {
// error can be due to abort
}
})();
}, [projectId]);

React.useEffect(() => {
const keyDownHandler = (e: KeyboardEvent) => {
if (
e.key === 'Escape' &&
(!document.activeElement || inputs.includes(document.activeElement.tagName.toLowerCase()))
) {
onClearTarget();
}
};

const focusHandler = (e: FocusEvent) => {
if (container?.contains(e.target as Node)) {
return;
}

if (
!e
.composedPath()
.filter((n) => n instanceof Element)
.map((n) => (n as Element).className)
.some((c) => c.indexOf(jsFieldToolbarMenuClassName) !== -1)
) {
onClearTarget();
}
};

document.addEventListener('focusin', focusHandler);
document.addEventListener('keydown', keyDownHandler);

return () => {
document.removeEventListener('focusin', focusHandler);
document.removeEventListener('keydown', keyDownHandler);
};
}, [container, onClearTarget]);

const onSelectToolbarMenuItem = React.useCallback(
(text: string) => {
if (typeof target?.selectionStart === 'number') {
const start = target.selectionStart;
const end = typeof target?.selectionEnd === 'number' ? target.selectionEnd : target.selectionStart;

const updatedItem = [value.slice(0, start), text, value.slice(end)].join('');
onChange(updatedItem);

setTimeout(() => {
target.setSelectionRange(updatedItem.length, updatedItem.length);
}, 0);
}

target?.focus();
},
[target, value, onChange]
);
return target ? (
<Callout
doNotLayer
directionalHint={DirectionalHint.topLeftEdge}
gapSpace={2}
isBeakVisible={false}
target={target}
>
<FieldToolbar
key="field-toolbar"
dismissHandlerClassName={jsFieldToolbarMenuClassName}
excludedToolbarItems={['template']}
properties={memoryVariables}
onSelectToolbarMenuItem={onSelectToolbarMenuItem}
/>
</Callout>
) : null;
};

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import React, { useRef, useState } from 'react';

import { getIntellisenseUrl } from '../../utils/getIntellisenseUrl';
import { ExpressionSwitchWindow } from '../expressions/ExpressionSwitchWindow';
import { ExpressionsListMenu } from '../expressions/ExpressionsListMenu';
import { ExpressionFieldToolbar } from '../expressions/ExpressionFieldToolbar';

import { JsonField } from './JsonField';
import { NumberField } from './NumberField';
Expand Down Expand Up @@ -68,23 +68,25 @@ export const IntellisenseExpressionField: React.FC<FieldProps<string>> = (props)
const scopes = ['expressions', 'user-variables'];
const intellisenseServerUrlRef = useRef(getIntellisenseUrl());

const [expressionsListContainerElements, setExpressionsListContainerElements] = useState<HTMLDivElement[]>([]);
const [containerElm, setContainerElm] = useState<HTMLDivElement | null>(null);
const [toolbarTargetElm, setToolbarTargetElm] = useState<HTMLInputElement | HTMLTextAreaElement | null>(null);

const focus = React.useCallback(
(id: string, value?: string, event?: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
if (event?.target) {
event.stopPropagation();
setToolbarTargetElm(event.target as HTMLInputElement | HTMLTextAreaElement);
}
},
[]
);

const completionListOverrideResolver = (value: string) => {
return value === '=' ? (
<ExpressionsListMenu
onExpressionSelected={(expression: string) => onChange(expression)}
onMenuMount={(refs) => {
setExpressionsListContainerElements(refs);
}}
/>
) : null;
};
const onClearTarget = React.useCallback(() => {
setToolbarTargetElm(null);
}, []);

return (
<Intellisense
completionListOverrideContainerElements={expressionsListContainerElements}
completionListOverrideResolver={completionListOverrideResolver}
focused={defaultFocused}
id={`intellisense-${id}`}
scopes={scopes}
Expand All @@ -102,18 +104,28 @@ export const IntellisenseExpressionField: React.FC<FieldProps<string>> = (props)
onKeyUpTextField,
onClickTextField,
}) => (
<StringField
{...props}
cursorPosition={cursorPosition}
focused={focused}
id={id}
value={textFieldValue}
onBlur={noop} // onBlur managed by Intellisense
onChange={(newValue) => onValueChanged(newValue || '')}
onClick={onClickTextField}
onKeyDown={onKeyDownTextField}
onKeyUp={onKeyUpTextField}
/>
<div ref={setContainerElm}>
<StringField
{...props}
cursorPosition={cursorPosition}
focused={focused}
id={id}
value={textFieldValue}
onBlur={noop} // onBlur managed by Intellisense
onChange={(newValue) => onValueChanged(newValue || '')}
onClick={onClickTextField}
onFocus={focus}
onKeyDown={onKeyDownTextField}
onKeyUp={onKeyUpTextField}
/>
<ExpressionFieldToolbar
container={containerElm}
target={toolbarTargetElm}
value={textFieldValue}
onChange={onChange}
onClearTarget={onClearTarget}
/>
</div>
)}
</Intellisense>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export const StringField: React.FC<FieldProps<string>> = function StringField(pr
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
if (typeof onFocus === 'function') {
e.stopPropagation();
onFocus(id, value);
onFocus(id, value, e);
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('<StringField />', () => {
const input = getByLabelText('a label');

fireEvent.focus(input);
expect(onFocus).toHaveBeenCalledWith('string field', 'string value');
expect(onFocus).toHaveBeenCalledWith('string field', 'string value', expect.any(Object));

fireEvent.blur(input);
expect(onBlur).toHaveBeenCalledWith('string field', 'string value');
Expand Down
2 changes: 1 addition & 1 deletion Composer/packages/extension-client/src/types/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export interface FieldProps<T = any> {
cursorPosition?: number;

onChange: ChangeHandler<T>;
onFocus?: (id: string, value?: T) => void;
onFocus?: (id: string, value?: T, event?: React.FocusEvent<any>) => void;
onBlur?: (id: string, value?: T) => void;

onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
Expand Down
11 changes: 4 additions & 7 deletions Composer/packages/intellisense/src/components/Intellisense.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const Intellisense = React.memo(
if (didComplete.current) {
didComplete.current = false;
} else {
if (completionItems && completionItems.length) {
if (completionItems?.length) {
setShowCompletionList(true);
} else {
setShowCompletionList(false);
Expand All @@ -93,24 +93,21 @@ export const Intellisense = React.memo(
shouldBlur = false;
}

if (
completionListOverrideContainerElements &&
completionListOverrideContainerElements.some((item) => !checkIsOutside(x, y, item))
) {
if (completionListOverrideContainerElements?.some((item) => !checkIsOutside(x, y, item))) {
shouldBlur = false;
}

if (shouldBlur) {
setShowCompletionList(false);
setCursorPosition(-1);
onBlur && onBlur(id);
onBlur?.(id);
}
};

const keydownHandler = (event: KeyboardEvent) => {
if ((event.key === 'Escape' || event.key === 'Tab') && focused) {
setShowCompletionList(false);
onBlur && onBlur(id);
onBlur?.(id);
}
};

Expand Down
Loading