diff --git a/src/components/composer/standalone_composer/standalone_composer.ts b/src/components/composer/standalone_composer/standalone_composer.ts index 730b50d960..6464914c90 100644 --- a/src/components/composer/standalone_composer/standalone_composer.ts +++ b/src/components/composer/standalone_composer/standalone_composer.ts @@ -1,7 +1,7 @@ import { cssPropertiesToCss } from "@odoo/o-spreadsheet-engine/components/helpers/css"; import { Token } from "@odoo/o-spreadsheet-engine/formulas/tokenizer"; import { SpreadsheetChildEnv } from "@odoo/o-spreadsheet-engine/types/spreadsheet_env"; -import { Component } from "@odoo/owl"; +import { Component, onMounted } from "@odoo/owl"; import { AutoCompleteProviderDefinition } from "../../../registries/auto_completes"; import { Store, useLocalStore, useStore } from "../../../store_engine"; import { Color, ComposerFocusType, UID } from "../../../types/index"; @@ -21,6 +21,7 @@ interface Props { title?: string; class?: string; invalid?: boolean; + autofocus?: boolean; getContextualColoredSymbolToken?: (token: Token) => Color; } @@ -36,6 +37,7 @@ export class StandaloneComposer extends Component { title: { type: String, optional: true }, class: { type: String, optional: true }, invalid: { type: Boolean, optional: true }, + autofocus: { type: Boolean, optional: true }, getContextualColoredSymbolToken: { type: Function, optional: true }, }; static components = { Composer }; @@ -69,6 +71,12 @@ export class StandaloneComposer extends Component { setCurrentContent: this.standaloneComposerStore.setCurrentContent, stopEdition: this.standaloneComposerStore.stopEdition, }; + onMounted(() => { + if (this.props.autofocus && this.focus === "inactive") { + this.composerFocusStore.focusComposer(this.composerInterface, {}); + this.composerFocusStore.activeComposer.editionMode; + } + }); } get focus(): ComposerFocusType { diff --git a/src/components/filters/filter_menu_criterion/filter_menu_criterion.xml b/src/components/filters/filter_menu_criterion/filter_menu_criterion.xml index 56215fc0a2..4b2339ef3a 100644 --- a/src/components/filters/filter_menu_criterion/filter_menu_criterion.xml +++ b/src/components/filters/filter_menu_criterion/filter_menu_criterion.xml @@ -13,6 +13,7 @@ criterion="state.criterion" onCriterionChanged.bind="onCriterionChanged" disableFormulas="true" + autofocus="true" /> diff --git a/src/components/selection_input/selection_input.ts b/src/components/selection_input/selection_input.ts index e751c7b73f..a9c272f1fe 100644 --- a/src/components/selection_input/selection_input.ts +++ b/src/components/selection_input/selection_input.ts @@ -12,6 +12,7 @@ interface Props { ranges: string[]; hasSingleRange?: boolean; required?: boolean; + autofocus?: boolean; isInvalid?: boolean; class?: string; onSelectionChanged?: (ranges: string[]) => void; @@ -50,6 +51,7 @@ export class SelectionInput extends Component { ranges: Array, hasSingleRange: { type: Boolean, optional: true }, required: { type: Boolean, optional: true }, + autofocus: { type: Boolean, optional: true }, isInvalid: { type: Boolean, optional: true }, class: { type: String, optional: true }, onSelectionChanged: { type: Function, optional: true }, @@ -105,6 +107,9 @@ export class SelectionInput extends Component { this.props.colors, this.props.disabledRanges ); + if (this.props.autofocus) { + this.store.focusById(this.store.selectionInputs[0]?.id); + } onWillUpdateProps((nextProps) => { if (nextProps.ranges.join() !== this.store.selectionInputValues.join()) { this.triggerChange(); diff --git a/src/components/side_panel/conditional_formatting/cf_editor/cell_is_rule_editor.xml b/src/components/side_panel/conditional_formatting/cf_editor/cell_is_rule_editor.xml index c59d82c7d7..bd7d2c083e 100644 --- a/src/components/side_panel/conditional_formatting/cf_editor/cell_is_rule_editor.xml +++ b/src/components/side_panel/conditional_formatting/cf_editor/cell_is_rule_editor.xml @@ -29,6 +29,7 @@ t-key="state.rules.cellIs.operator" criterion="genericCriterion" onCriterionChanged.bind="onRuleValuesChanged" + autofocus="this.state.hasEditedCf" />
Formatting style
diff --git a/src/components/side_panel/criterion_form/criterion_form.ts b/src/components/side_panel/criterion_form/criterion_form.ts index dc0e26ebd4..3a2304c61f 100644 --- a/src/components/side_panel/criterion_form/criterion_form.ts +++ b/src/components/side_panel/criterion_form/criterion_form.ts @@ -1,5 +1,5 @@ import { SpreadsheetChildEnv } from "@odoo/o-spreadsheet-engine/types/spreadsheet_env"; -import { Component, onMounted } from "@odoo/owl"; +import { Component } from "@odoo/owl"; import { useStore } from "../../../store_engine"; import { GenericCriterion } from "../../../types"; import { ComposerFocusStore } from "../../composer/composer_focus_store"; @@ -8,6 +8,7 @@ interface Props { criterion: T; onCriterionChanged: (criterion: T) => void; disableFormulas?: boolean; + autofocus?: boolean; } export abstract class CriterionForm< @@ -17,12 +18,13 @@ export abstract class CriterionForm< criterion: Object, onCriterionChanged: Function, disableFormulas: { type: Boolean, optional: true }, + autofocus: { type: Boolean, optional: true }, }; setup() { const composerFocusStore = useStore(ComposerFocusStore); - onMounted(() => { + if (composerFocusStore.activeComposer.editionMode !== "inactive") { composerFocusStore.activeComposer.stopEdition(); - }); + } } updateCriterion(criterion: Partial) { diff --git a/src/components/side_panel/criterion_form/criterion_input/criterion_input.ts b/src/components/side_panel/criterion_form/criterion_input/criterion_input.ts index 92b200742e..3265e0e59f 100644 --- a/src/components/side_panel/criterion_form/criterion_input/criterion_input.ts +++ b/src/components/side_panel/criterion_form/criterion_input/criterion_input.ts @@ -41,8 +41,8 @@ export class CriterionInput extends Component { setup() { useEffect( () => { - if (this.props.focused) { - this.inputRef.el!.focus(); + if (this.props.focused && this.inputRef.el) { + this.inputRef.el.focus(); } }, () => [this.props.focused, this.inputRef.el] @@ -94,6 +94,7 @@ export class CriterionInput extends Component { defaultRangeSheetId: this.env.model.getters.getActiveSheetId(), invalid: this.state.shouldDisplayError && !!this.errorMessage, defaultStatic: true, + autofocus: this.props.focused, }; } diff --git a/src/components/side_panel/criterion_form/date_criterion/date_criterion.xml b/src/components/side_panel/criterion_form/date_criterion/date_criterion.xml index e6c5adeb1e..e296650f66 100644 --- a/src/components/side_panel/criterion_form/date_criterion/date_criterion.xml +++ b/src/components/side_panel/criterion_form/date_criterion/date_criterion.xml @@ -17,6 +17,7 @@ onValueChanged.bind="onValueChanged" criterionType="props.criterion.type" disableFormulas="props.disableFormulas" + focused="props.autofocus" /> diff --git a/src/components/side_panel/criterion_form/double_input_criterion/double_input_criterion.xml b/src/components/side_panel/criterion_form/double_input_criterion/double_input_criterion.xml index 16dcc59602..9beb6d81f4 100644 --- a/src/components/side_panel/criterion_form/double_input_criterion/double_input_criterion.xml +++ b/src/components/side_panel/criterion_form/double_input_criterion/double_input_criterion.xml @@ -5,6 +5,7 @@ onValueChanged.bind="onFirstValueChanged" criterionType="props.criterion.type" disableFormulas="props.disableFormulas" + focused="props.autofocus" /> diff --git a/src/components/side_panel/criterion_form/value_in_list_criterion/value_in_list_criterion.ts b/src/components/side_panel/criterion_form/value_in_list_criterion/value_in_list_criterion.ts index 0fe891f20e..874321fed7 100644 --- a/src/components/side_panel/criterion_form/value_in_list_criterion/value_in_list_criterion.ts +++ b/src/components/side_panel/criterion_form/value_in_list_criterion/value_in_list_criterion.ts @@ -15,6 +15,7 @@ export class ListCriterionForm extends CriterionForm { state = useState({ numberOfValues: Math.max(this.props.criterion.values.length, 2), + focusedValueIndex: this.props.autofocus ? 0 : undefined, }); setup() { diff --git a/src/components/side_panel/criterion_form/value_in_range_criterion/value_in_range_criterion.xml b/src/components/side_panel/criterion_form/value_in_range_criterion/value_in_range_criterion.xml index 5ae27b3fff..bde59ac2b9 100644 --- a/src/components/side_panel/criterion_form/value_in_range_criterion/value_in_range_criterion.xml +++ b/src/components/side_panel/criterion_form/value_in_range_criterion/value_in_range_criterion.xml @@ -5,6 +5,7 @@ onSelectionChanged="(ranges) => this.onRangeChanged(ranges[0])" required="true" hasSingleRange="true" + autofocus="props.autofocus" />
diff --git a/src/components/side_panel/data_validation/dv_editor/dv_editor.ts b/src/components/side_panel/data_validation/dv_editor/dv_editor.ts index 5ad7e61d68..b7e3d83aa4 100644 --- a/src/components/side_panel/data_validation/dv_editor/dv_editor.ts +++ b/src/components/side_panel/data_validation/dv_editor/dv_editor.ts @@ -33,6 +33,7 @@ interface Props { interface State { rule: DataValidationRuleData; errors: CancelledReason[]; + isTypeUpdated: boolean; } export class DataValidationEditor extends Component { @@ -44,7 +45,11 @@ export class DataValidationEditor extends Component onCloseSidePanel: { type: Function, optional: true }, }; - state = useState({ rule: this.defaultDataValidationRule, errors: [] }); + state = useState({ + rule: this.defaultDataValidationRule, + errors: [], + isTypeUpdated: false, + }); private editingSheetId!: UID; setup() { @@ -62,6 +67,7 @@ export class DataValidationEditor extends Component onCriterionTypeChanged(type: DataValidationCriterionType) { this.state.rule.criterion.type = type; + this.state.isTypeUpdated = true; } onRangesChanged(ranges: string[]) { diff --git a/src/components/side_panel/data_validation/dv_editor/dv_editor.xml b/src/components/side_panel/data_validation/dv_editor/dv_editor.xml index 658e7a02e3..46962dfccb 100644 --- a/src/components/side_panel/data_validation/dv_editor/dv_editor.xml +++ b/src/components/side_panel/data_validation/dv_editor/dv_editor.xml @@ -23,6 +23,7 @@ t-key="state.rule.criterion.type" criterion="state.rule.criterion" onCriterionChanged.bind="onCriterionChanged" + autofocus="state.isTypeUpdated" />
diff --git a/tests/conditional_formatting/conditional_formatting_panel_component.test.ts b/tests/conditional_formatting/conditional_formatting_panel_component.test.ts index b835f5f3a9..f28161fc7e 100644 --- a/tests/conditional_formatting/conditional_formatting_panel_component.test.ts +++ b/tests/conditional_formatting/conditional_formatting_panel_component.test.ts @@ -240,7 +240,9 @@ describe("UI of conditional formats", () => { // change every value setInputValueAndTrigger(selectors.ruleEditor.range, "A1:A3"); + expect(".o-composer").not.toHaveClass("active"); await changeRuleOperatorType(fixture, "beginsWithText"); + expect(".o-composer").toHaveClass("active"); editStandaloneComposer(selectors.ruleEditor.editor.valueInput, "3"); await click(fixture, selectors.ruleEditor.editor.bold); diff --git a/tests/data_validation/data_validation_generics_side_panel_component.test.ts b/tests/data_validation/data_validation_generics_side_panel_component.test.ts index 7f4672eba6..4fcb93ad88 100644 --- a/tests/data_validation/data_validation_generics_side_panel_component.test.ts +++ b/tests/data_validation/data_validation_generics_side_panel_component.test.ts @@ -141,6 +141,15 @@ describe("data validation sidePanel component", () => { ]); }); + test("date input is focused when criterion type is changed", async () => { + await simulateClick(".o-dv-add"); + expect(document.activeElement).not.toBe(fixture.querySelector(".o-dv-input .o-composer")); + await changeCriterionType("dateIs"); + setInputValueAndTrigger(".o-dv-date-value", "exactDate"); + expect(".o-dv-input .o-composer").toHaveClass("active"); + expect(document.activeElement).toBe(fixture.querySelector(".o-dv-input .o-composer")); + }); + test("Invalid range", async () => { await simulateClick(".o-dv-add"); await nextTick(); @@ -248,6 +257,28 @@ describe("data validation sidePanel component", () => { ); }); + test("single input is focused when changing type", async () => { + await simulateClick(".o-dv-add"); + await nextTick(); + expect(".o-dv-input .o-composer.active").toHaveCount(0); + await changeCriterionType("isEqualText"); + expect(".o-dv-input .o-composer.active").toHaveCount(1); + expect(document.activeElement).toBe(fixture.querySelector(".o-dv-input .o-composer")); + expect(".o-dv-input .o-composer").toHaveClass("active"); + }); + + test("first input of two is focused when changing type", async () => { + await simulateClick(".o-dv-add"); + await nextTick(); + expect(".o-dv-input .o-composer.active").toHaveCount(0); + await changeCriterionType("isBetween"); + expect(".o-dv-input .o-composer.active").toHaveCount(1); + const composers = fixture.querySelectorAll(".o-dv-input .o-composer"); + expect(document.activeElement).toBe(composers[0]); + expect(composers[0]).toHaveClass("active"); + expect(composers[1]).not.toHaveClass("active"); + }); + test("Can make the rule blocking", async () => { await simulateClick(".o-dv-add"); await nextTick(); diff --git a/tests/data_validation/data_validation_list_component.test.ts b/tests/data_validation/data_validation_list_component.test.ts index 15df701c7d..e2f6ce474d 100644 --- a/tests/data_validation/data_validation_list_component.test.ts +++ b/tests/data_validation/data_validation_list_component.test.ts @@ -82,6 +82,15 @@ describe("Edit criterion in side panel", () => { expect(inputs[0].innerText).toBe("hola"); }); + test("first input is focused when criterion type is changed", async () => { + expect(document.activeElement).not.toBe(fixture.querySelector(".o-dv-input input")); + await click(fixture, ".o-dv-type"); + await click(fixture, `.o-menu-item[data-name="containsText"]`); + await click(fixture, ".o-dv-type"); + await click(fixture, `.o-menu-item[data-name="isValueInList"]`); + expect(document.activeElement).toBe(fixture.querySelector(".o-dv-input input")); + }); + test("Can add a new value", async () => { await click(fixture, ".o-dv-list-add-value"); const inputs = fixture.querySelectorAll(".o-dv-list-values .o-input"); @@ -202,6 +211,15 @@ describe("Edit criterion in side panel", () => { expect(getDataValidationRules(model)[0].criterion.values).toEqual(["B1:B9"]); }); + test("range input is focused when criterion type is changed", async () => { + expect(".o-dv-settings .o-selection-input input").not.toHaveClass("o-focused"); + await click(fixture, ".o-dv-type"); + await click(fixture, `.o-menu-item[data-name="containsText"]`); + await click(fixture, ".o-dv-type"); + await click(fixture, `.o-menu-item[data-name="isValueInRange"]`); + expect(".o-dv-settings .o-selection-input input").toHaveClass("o-focused"); + }); + test("Can change display style", () => { const displayStyleInput = fixture.querySelector(".o-dv-display-style"); setInputValueAndTrigger(displayStyleInput, "plainText"); diff --git a/tests/setup/jest_extend.ts b/tests/setup/jest_extend.ts index a591316074..fa6c30b10d 100644 --- a/tests/setup/jest_extend.ts +++ b/tests/setup/jest_extend.ts @@ -268,16 +268,26 @@ CancelledReasons: ${this.utils.printReceived(dispatchResult.reasons)} return { pass: false, message: () => message }; } const pass = element.classList.contains(expectedClass); - const message = () => - pass - ? "" - : `expect(target).toHaveClass(expected);\n\n${this.utils.printDiffOrStringify( - expectedClass, - element.className, - "Expected class", - "Received class", - false - )}`; + const message = () => { + if (this.isNot && pass) { + return `expect(target).not.toHaveClass(expected);\n\n${this.utils.printDiffOrStringify( + expectedClass, + element.className, + "Unexpected class", + "Received class", + false + )}`; + } else if (!pass) { + return `expect(target).toHaveClass(expected);\n\n${this.utils.printDiffOrStringify( + expectedClass, + element.className, + "Expected class", + "Received class", + false + )}`; + } + return ""; + }; return { pass, message }; }, toHaveAttribute(target: DOMTarget, attribute: string, expectedValue: string) { diff --git a/tests/table/filter_menu_component.test.ts b/tests/table/filter_menu_component.test.ts index 7585218cf5..4fdf46c6ea 100644 --- a/tests/table/filter_menu_component.test.ts +++ b/tests/table/filter_menu_component.test.ts @@ -444,6 +444,7 @@ describe("Filter menu component", () => { await simulateClick(".o-filter-criterion-type"); await simulateClick(".o-menu-item[data-name='containsText']"); + expect(document.activeElement).toBe(fixture.querySelector(".o-dv-input input")); await setInputValueAndTrigger(".o-dv-input input", "hello"); await simulateClick(".o-filter-menu-confirm");