feat: [Context Switching] maintain focus of code editor fields (#18240)

This commit is contained in:
akash-codemonk 2022-12-15 19:45:46 +05:30 committed by GitHub
parent c72f865962
commit 17dbe63ed3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 121 additions and 78 deletions

View File

@ -31,7 +31,7 @@ describe("Canvas context Property Pane", function() {
cy.focusCodeInput(propertyControlSelector);
},
() => {
cy.assertSoftFocusOnPropertyPane(propertyControlSelector);
cy.assertCursorOnCodeInput(propertyControlSelector);
},
"Button1",
);
@ -160,7 +160,7 @@ describe("Canvas context Property Pane", function() {
cy.focusCodeInput(propertyControlSelector);
},
() => {
cy.assertSoftFocusOnPropertyPane(propertyControlSelector);
cy.assertCursorOnCodeInput(propertyControlSelector);
},
"Table1",
);
@ -241,7 +241,7 @@ describe("Canvas context Property Pane", function() {
cy.focusCodeInput(propertyControlSelector);
},
() => {
cy.assertSoftFocusOnPropertyPane(propertyControlSelector);
cy.assertCursorOnCodeInput(propertyControlSelector);
},
"Table1",
);

View File

@ -1,5 +1,6 @@
import reconnectDatasourceModal from "../../../../locators/ReconnectLocators";
const apiwidget = require("../../../../locators/apiWidgetslocator.json");
const queryLocators = require("../../../../locators/QueryEditor.json");
import { ObjectsRegistry } from "../../../../support/Objects/Registry";
const homePage = ObjectsRegistry.HomePage;
@ -88,7 +89,7 @@ describe("MaintainContext&Focus", function() {
cy.get(`.t--entity-name:contains("Page1")`).click();
cy.get(".t--widget-name").should("have.text", "Text1");
cy.assertSoftFocusOnPropertyPane(".t--property-control-text", {
cy.assertCursorOnCodeInput(".t--property-control-text", {
ch: 2,
line: 0,
});
@ -195,4 +196,18 @@ describe("MaintainContext&Focus", function() {
// Validate if section with index 1 is expanded
dataSources.AssertSectionCollapseState(1, false);
});
it("10. Maintain focus of form control inputs", () => {
ee.SelectEntityByName("SQL_Query");
dataSources.ToggleUsePreparedStatement(false);
cy.SearchEntityandOpen("S3_Query");
cy.get(queryLocators.querySettingsTab).click();
cy.setQueryTimeout(10000);
cy.SearchEntityandOpen("SQL_Query");
cy.get(".t--form-control-SWITCH input").should("be.focused");
cy.SearchEntityandOpen("S3_Query");
cy.get(queryLocators.querySettingsTab).click();
cy.xpath(queryLocators.queryTimeout).should("be.focused");
});
});

View File

@ -617,7 +617,7 @@ Cypress.Commands.add(
);
Cypress.Commands.add(
"assertSoftFocusOnPropertyPane",
"assertSoftFocusOnCodeInput",
($selector, cursor = { ch: 0, line: 0 }) => {
cy.EnableAllCodeEditors();
cy.get($selector)

View File

@ -5,9 +5,9 @@ import {
PropertyPanelContext,
} from "reducers/uiReducers/editorContextReducer";
export const setFocusableCodeEditorField = (path: string | undefined) => {
export const setFocusableInputField = (path: string | undefined) => {
return {
type: ReduxActionTypes.SET_FOCUSABLE_CODE_EDITOR_FIELD,
type: ReduxActionTypes.SET_FOCUSABLE_INPUT_FIELD,
payload: { path },
};
};

View File

@ -60,13 +60,6 @@ export const setSelectedPropertyTabIndex = (selectedIndex: number) => {
};
};
export const setFocusablePropertyPaneField = (path?: string) => {
return {
type: ReduxActionTypes.SET_FOCUSABLE_PROPERTY_FIELD,
payload: { path },
};
};
export const setSelectedPropertyPanel = (
path: string | undefined,
index: number,

View File

@ -687,10 +687,6 @@ export const ReduxActionTypes = {
FETCH_CURRENT_TENANT_CONFIG: "FETCH_CURRENT_TENANT_CONFIG",
FETCH_CURRENT_TENANT_CONFIG_SUCCESS: "FETCH_CURRENT_TENANT_CONFIG_SUCCESS",
SET_FOCUS_HISTORY: "SET_FOCUS_HISTORY",
SET_FOCUSABLE_CODE_EDITOR_FIELD: "SET_FOCUSABLE_CODE_EDITOR_FIELD",
GENERATE_KEY_AND_SET_FOCUSABLE_CODE_EDITOR_FIELD:
"GENERATE_KEY_AND_SET_FOCUSABLE_CODE_EDITOR_FIELD",
SET_FOCUSABLE_PROPERTY_FIELD: "SET_FOCUSABLE_PROPERTY_FIELD",
ROUTE_CHANGED: "ROUTE_CHANGED",
PAGE_CHANGED: "PAGE_CHANGED",
SET_API_PANE_CONFIG_SELECTED_TAB: "SET_API_PANE_CONFIG_SELECTED_TAB",
@ -698,6 +694,7 @@ export const ReduxActionTypes = {
SET_API_PANE_RESPONSE_PANE_HEIGHT: "SET_API_PANE_RESPONSE_PANE_HEIGHT",
SET_API_RIGHT_PANE_SELECTED_TAB: "SET_API_RIGHT_PANE_SELECTED_TAB",
SET_EDITOR_FIELD_FOCUS: "SET_EDITOR_FIELD_FOCUS",
SET_FOCUSABLE_INPUT_FIELD: "SET_FOCUSABLE_INPUT_FIELD",
SET_CODE_EDITOR_CURSOR: "SET_CODE_EDITOR_CURSOR",
SET_CODE_EDITOR_CURSOR_HISTORY: "SET_CODE_EDITOR_CURSOR_HISTORY",
SET_EVAL_POPUP_STATE: "SET_EVAL_POPUP_STATE",

View File

@ -105,7 +105,7 @@ import { interactionAnalyticsEvent } from "utils/AppsmithUtils";
import { AdditionalDynamicDataTree } from "utils/autocomplete/customTreeTypeDefCreator";
import {
getCodeEditorLastCursorPosition,
getIsCodeEditorFocused,
getIsInputFieldFocused,
} from "selectors/editorContextSelectors";
import {
CodeEditorFocusState,
@ -1153,7 +1153,7 @@ const mapStateToProps = (state: AppState, props: EditorProps) => ({
pluginIdToImageLocation: getPluginIdToImageLocation(state),
recentEntities: getRecentEntityIds(state),
lintErrors: getEntityLintErrors(state, props.dataTreePath),
editorIsFocused: getIsCodeEditorFocused(state, getEditorIdentifier(props)),
editorIsFocused: getIsInputFieldFocused(state, getEditorIdentifier(props)),
editorLastCursorPosition: getCodeEditorLastCursorPosition(
state,
getEditorIdentifier(props),

View File

@ -19,7 +19,7 @@ import {
getCodeEditorHistory,
getExplorerSwitchIndex,
getPropertyPanelState,
getFocusableCodeEditorField,
getFocusableInputField,
getSelectedCanvasDebuggerTab,
getWidgetSelectedPropertyTabIndex,
} from "selectors/editorContextSelectors";
@ -31,7 +31,7 @@ import {
setPanelPropertiesState,
setWidgetSelectedPropertyTabIndex,
} from "actions/editorContextActions";
import { setFocusableCodeEditorField } from "actions/editorContextActions";
import { setFocusableInputField } from "actions/editorContextActions";
import {
getAllDatasourceCollapsibleState,
getSelectedWidgets,
@ -75,17 +75,13 @@ import {
setPropertyPaneWidthAction,
setSelectedPropertyPanels,
} from "actions/propertyPaneActions";
import {
setAllPropertySectionState,
setFocusablePropertyPaneField,
} from "actions/propertyPaneActions";
import { setAllPropertySectionState } from "actions/propertyPaneActions";
import { setCanvasDebuggerSelectedTab } from "actions/debuggerActions";
import {
setAllDatasourceCollapsible,
setDatasourceViewMode,
} from "actions/datasourceActions";
import { PluginPackageName } from "entities/Action";
import { getFocusablePropertyPaneField } from "selectors/propertyPaneSelectors";
export enum FocusElement {
ApiPaneConfigTabs = "ApiPaneConfigTabs",
@ -105,8 +101,6 @@ export enum FocusElement {
JSPaneConfigTabs = "JSPaneConfigTabs",
JSPaneResponseTabs = "JSPaneResponseTabs",
JSPaneResponseHeight = "JSPaneResponseHeight",
CodeEditor = "CodeEditor",
PropertyField = "PropertyField",
PropertySections = "PropertySections",
PropertyTabs = "PropertyTabs",
PropertyPanelContext = "PropertyPanelContext",
@ -114,6 +108,7 @@ export enum FocusElement {
SelectedPropertyPanel = "SelectedPropertyPanel",
SelectedWidgets = "SelectedWidgets",
SubEntityCollapsibleState = "SubEntityCollapsibleState",
InputField = "InputField",
}
type Config = {
@ -211,9 +206,9 @@ export const FocusElementsConfig: Record<FocusEntity, Config[]> = {
],
[FocusEntity.JS_OBJECT]: [
{
name: FocusElement.CodeEditor,
selector: getFocusableCodeEditorField,
setter: setFocusableCodeEditorField,
name: FocusElement.InputField,
selector: getFocusableInputField,
setter: setFocusableInputField,
},
{
name: FocusElement.JSPaneConfigTabs,
@ -236,9 +231,9 @@ export const FocusElementsConfig: Record<FocusEntity, Config[]> = {
],
[FocusEntity.QUERY]: [
{
name: FocusElement.CodeEditor,
selector: getFocusableCodeEditorField,
setter: setFocusableCodeEditorField,
name: FocusElement.InputField,
selector: getFocusableInputField,
setter: setFocusableInputField,
},
{
name: FocusElement.QueryPaneConfigTabs,
@ -267,18 +262,13 @@ export const FocusElementsConfig: Record<FocusEntity, Config[]> = {
defaultValue: 0,
},
{
name: FocusElement.PropertyField,
selector: getFocusablePropertyPaneField,
setter: setFocusablePropertyPaneField,
name: FocusElement.InputField,
selector: getFocusableInputField,
setter: setFocusableInputField,
defaultValue: "",
},
],
[FocusEntity.API]: [
{
name: FocusElement.CodeEditor,
selector: getFocusableCodeEditorField,
setter: setFocusableCodeEditorField,
},
{
name: FocusElement.ApiPaneConfigTabs,
selector: getApiPaneConfigSelectedTabIndex,
@ -302,6 +292,11 @@ export const FocusElementsConfig: Record<FocusEntity, Config[]> = {
setter: setApiPaneResponsePaneHeight,
defaultValue: ActionExecutionResizerHeight,
},
{
name: FocusElement.InputField,
selector: getFocusableInputField,
setter: setFocusableInputField,
},
{
name: FocusElement.ApiRightPaneTabs,
selector: getApiRightPaneSelectedTab,

View File

@ -17,6 +17,7 @@ import lightmodeThumbnail from "assets/icons/gifs/lightmode_thumbnail.png";
import darkmodeThumbnail from "assets/icons/gifs/darkmode_thumbnail.png";
interface PaginationProps {
actionName: string;
onTestClick: (test?: "PREV" | "NEXT") => void;
paginationType: PaginationType;
theme?: EditorTheme;
@ -183,6 +184,7 @@ export default function Pagination(props: PaginationProps) {
border={CodeEditorBorder.ALL_SIDE}
className="t--apiFormPaginationPrev"
fill={!!true}
focusElementName={`${props.actionName}.actionConfiguration.prev`}
height="100%"
name="actionConfiguration.prev"
theme={props.theme}
@ -208,6 +210,7 @@ export default function Pagination(props: PaginationProps) {
border={CodeEditorBorder.ALL_SIDE}
className="t--apiFormPaginationNext"
fill={!!true}
focusElementName={`${props.actionName}.actionConfiguration.next`}
height="100%"
name="actionConfiguration.next"
theme={props.theme}

View File

@ -256,6 +256,7 @@ function RapidApiEditorForm(props: Props) {
title: "Pagination",
panelComponent: (
<Pagination
actionName={props.apiName}
onTestClick={props.onRunClick}
paginationType={props.paginationType}
/>

View File

@ -63,6 +63,7 @@ function ApiEditorForm(props: Props) {
formName={API_EDITOR_FORM_NAME}
paginationUIComponent={
<Pagination
actionName={actionName}
onTestClick={props.onRunClick}
paginationType={props.paginationType}
theme={theme}

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect, useRef } from "react";
import { ControlProps } from "components/formControls/BaseControl";
import {
EvaluationError,
@ -18,6 +18,15 @@ import { FormIcons } from "icons/FormIcons";
import { FormControlProps } from "./FormControl";
import { ToggleComponentToJsonHandler } from "components/editorComponents/form/ToggleComponentToJson";
import styled from "styled-components";
import { useDispatch, useSelector } from "react-redux";
import { identifyEntityFromPath } from "navigation/FocusEntity";
import { AppState } from "@appsmith/reducers";
import {
getPropertyControlFocusElement,
shouldFocusOnPropertyControl,
} from "utils/editorContextUtils";
import { getIsInputFieldFocused } from "selectors/editorContextSelectors";
import { setFocusableInputField } from "actions/editorContextActions";
const FlexWrapper = styled.div`
display: flex;
@ -59,7 +68,48 @@ interface FormConfigProps extends FormControlProps {
// props.children will render the form element
export default function FormConfig(props: FormConfigProps) {
let top, bottom;
const controlRef = useRef<HTMLDivElement | null>(null);
const dispatch = useDispatch();
const entityInfo = identifyEntityFromPath(
window.location.pathname,
window.location.hash,
);
const handleOnFocus = () => {
if (props.config.configProperty) {
// Need an additional identifier to trigger another render when configProperty
// are same for two different entitites
dispatch(
setFocusableInputField(
`${entityInfo.id}.${props.config.configProperty}`,
),
);
}
};
const shouldFocusPropertyPath: boolean = useSelector((state: AppState) =>
getIsInputFieldFocused(
state,
`${entityInfo.id}.${props.config.configProperty}`,
),
);
useEffect(() => {
if (shouldFocusPropertyPath) {
setTimeout(() => {
if (shouldFocusOnPropertyControl(controlRef.current)) {
const focusableElement = getPropertyControlFocusElement(
controlRef.current,
);
focusableElement?.scrollIntoView({
block: "center",
behavior: "smooth",
});
focusableElement?.focus();
}
}, 0);
}
}, [shouldFocusPropertyPath]);
if (props.multipleConfig?.length) {
top = (
<div style={{ display: "flex" }}>
@ -86,7 +136,11 @@ export default function FormConfig(props: FormConfigProps) {
return (
<div>
<FormConfigWrapper controlType={props.config.controlType}>
<FormConfigWrapper
controlType={props.config.controlType}
onFocus={handleOnFocus}
ref={controlRef}
>
{props.config.controlType === "CHECKBOX" ? (
<>
{props.children}

View File

@ -34,7 +34,6 @@ import {
THEME_BINDING_REGEX,
} from "utils/DynamicBindingUtils";
import {
getShouldFocusPropertyPath,
getWidgetPropsForPropertyName,
WidgetProperties,
} from "selectors/propertyPaneSelectors";
@ -55,8 +54,9 @@ import {
shouldFocusOnPropertyControl,
} from "utils/editorContextUtils";
import PropertyPaneHelperText from "./PropertyPaneHelperText";
import { setFocusablePropertyPaneField } from "actions/propertyPaneActions";
import WidgetFactory from "utils/WidgetFactory";
import { getIsInputFieldFocused } from "selectors/editorContextSelectors";
import { setFocusableInputField } from "actions/editorContextActions";
type Props = PropertyPaneControlConfig & {
panel: IPanelProps;
@ -91,7 +91,7 @@ const PropertyControl = memo((props: Props) => {
const hasDispatchedPropertyFocus = useRef<boolean>(false);
const shouldFocusPropertyPath: boolean = useSelector(
(state: AppState) =>
getShouldFocusPropertyPath(
getIsInputFieldFocused(
state,
dataTreePath,
hasDispatchedPropertyFocus.current,
@ -576,7 +576,7 @@ const PropertyControl = memo((props: Props) => {
if (!shouldFocusPropertyPath) {
hasDispatchedPropertyFocus.current = true;
setTimeout(() => {
dispatch(setFocusablePropertyPaneField(dataTreePath));
dispatch(setFocusableInputField(dataTreePath));
}, 0);
}
};

View File

@ -32,7 +32,7 @@ export type EditorContextState = {
entityCollapsibleFields: Record<string, boolean>;
subEntityCollapsibleFields: Record<string, boolean>;
explorerSwitchIndex: number;
focusableCodeEditor?: string;
focusedInputField?: string;
codeEditorHistory: Record<string, CodeEditorContext>;
propertySectionState: Record<string, boolean>;
selectedPropertyTabIndex: number;
@ -61,14 +61,14 @@ export const isSubEntities = (name: string): boolean => {
* Context Reducer to store states of different components of editor
*/
export const editorContextReducer = createImmerReducer(initialState, {
[ReduxActionTypes.SET_FOCUSABLE_CODE_EDITOR_FIELD]: (
[ReduxActionTypes.SET_FOCUSABLE_INPUT_FIELD]: (
state: EditorContextState,
action: {
payload: { path: string };
},
) => {
const { path } = action.payload;
state.focusableCodeEditor = path;
state.focusedInputField = path;
},
[ReduxActionTypes.SET_CODE_EDITOR_CURSOR]: (
state: EditorContextState,

View File

@ -76,12 +76,6 @@ const propertyPaneReducer = createImmerReducer(initialState, {
) => {
state.width = action.payload;
},
[ReduxActionTypes.SET_FOCUSABLE_PROPERTY_FIELD]: (
state: PropertyPaneReduxState,
action: ReduxAction<{ path: string }>,
) => {
state.focusedProperty = action.payload.path;
},
[ReduxActionTypes.SET_SELECTED_PANEL_PROPERTY]: (
state: PropertyPaneReduxState,
action: {

View File

@ -13,7 +13,7 @@ import { all, put, takeLatest } from "redux-saga/effects";
import {
CodeEditorFocusState,
setCodeEditorCursorAction,
setFocusableCodeEditorField,
setFocusableInputField,
} from "actions/editorContextActions";
import { FocusEntity, identifyEntityFromPath } from "navigation/FocusEntity";
@ -28,10 +28,11 @@ function* setEditorFieldFocus(action: ReduxAction<CodeEditorFocusState>) {
window.location.pathname,
window.location.hash,
);
const ignoredEntities = [FocusEntity.DATASOURCE];
if (key) {
if (entityInfo.entity !== FocusEntity.PROPERTY_PANE) {
yield put(setFocusableCodeEditorField(key));
if (!ignoredEntities.includes(entityInfo.entity)) {
yield put(setFocusableInputField(key));
}
yield put(setCodeEditorCursorAction(key, cursorPosition));
}

View File

@ -11,8 +11,8 @@ import { createSelector } from "reselect";
import { selectFeatureFlags } from "selectors/usersSelectors";
import FeatureFlags from "entities/FeatureFlags";
export const getFocusableCodeEditorField = (state: AppState) =>
state.ui.editorContext.focusableCodeEditor;
export const getFocusableInputField = (state: AppState) =>
state.ui.editorContext.focusedInputField;
export const getCodeEditorHistory = (state: AppState) =>
state.ui.editorContext.codeEditorHistory;
@ -67,9 +67,9 @@ export const getSelectedPropertyTabIndex = createSelector(
},
);
export const getIsCodeEditorFocused = createSelector(
export const getIsInputFieldFocused = createSelector(
[
getFocusableCodeEditorField,
getFocusableInputField,
selectFeatureFlags,
(_state: AppState, key: string | undefined) => key,
],

View File

@ -20,6 +20,7 @@ import { EVALUATION_PATH } from "utils/DynamicBindingUtils";
import { generateClassName } from "utils/generators";
import { getWidgets } from "sagas/selectors";
import { RegisteredWidgetFeatures } from "utils/WidgetFeatures";
import { getFocusableInputField } from "./editorContextSelectors";
export type WidgetProperties = WidgetProps & {
[EVALUATION_PATH]?: DataTreeEntity;
@ -266,18 +267,6 @@ export const getIsPropertyPaneVisible = createSelector(
export const getPropertyPaneWidth = (state: AppState) => {
return state.ui.propertyPane.width;
};
export const getFocusablePropertyPaneField = (state: AppState) =>
state.ui.propertyPane.focusedProperty;
export const getShouldFocusPropertyPath = createSelector(
[
getFocusablePropertyPaneField,
(_state: AppState, key: string | undefined) => key,
],
(focusableField: string | undefined, key: string | undefined): boolean => {
return !!(key && focusableField === key);
},
);
export const getSelectedPropertyPanelIndex = createSelector(
[
@ -296,7 +285,7 @@ export const getSelectedPropertyPanelIndex = createSelector(
export const getShouldFocusPropertySearch = createSelector(
getIsCurrentWidgetRecentlyAdded,
getFocusablePropertyPaneField,
getFocusableInputField,
(
isCurrentWidgetRecentlyAdded: boolean,
focusableField: string | undefined,