diff --git a/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx b/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx index aad5bf74713285475b59a88bb73a1a0fe91b1b29..9cbd31a43e1c4dbd7ca6f0a361f91c65beeeff0f 100644 --- a/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx +++ b/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx @@ -132,6 +132,12 @@ export default class ExpressionEditorTextfield extends React.Component { fontSize: "12px", }); + const passKeysToBrowser = editor.commands.byName.passKeysToBrowser; + editor.commands.bindKey("Tab", passKeysToBrowser); + editor.commands.bindKey("Shift-Tab", passKeysToBrowser); + editor.commands.removeCommand(editor.commands.byName.indent); + editor.commands.removeCommand(editor.commands.byName.outdent); + this.setCaretPosition( this.state.source.length, this.state.source.length === 0, @@ -218,14 +224,11 @@ export default class ExpressionEditorTextfield extends React.Component { } }; - handleTab = () => { + chooseSuggestion = () => { const { highlightedSuggestionIndex, suggestions } = this.state; - const { editor } = this.input.current; if (suggestions.length) { this.onSuggestionSelected(highlightedSuggestionIndex); - } else { - editor.commands.byName.tab(); } }; @@ -274,10 +277,31 @@ export default class ExpressionEditorTextfield extends React.Component { clearSuggestions() { this.setState({ - suggestions: [], highlightedSuggestionIndex: 0, helpText: null, }); + this.updateSuggestions([]); + } + + updateSuggestions(suggestions = []) { + this.setState({ suggestions }); + + // Correctly bind Tab depending on whether suggestions are available or not + if (this.input.current) { + const { editor } = this.input.current; + const { suggestions } = this.state; + const tabBinding = editor.commands.commandKeyBinding.tab; + if (suggestions.length > 0) { + // Something to suggest? Tab is for choosing one of them + editor.commands.bindKey("Tab", editor.commands.byName.chooseSuggestion); + } else { + if (Array.isArray(tabBinding) && tabBinding.length > 1) { + // No more suggestions? Keep a single binding and remove the + // second one (added to choose a suggestion) + editor.commands.commandKeyBinding.tab = tabBinding.shift(); + } + } + } } compileExpression() { @@ -338,10 +362,8 @@ export default class ExpressionEditorTextfield extends React.Component { targetOffset: cursor.column, }); - this.setState({ - suggestions: suggestions || [], - helpText, - }); + this.setState({ helpText }); + this.updateSuggestions(suggestions); } errorAsMarkers(errorMessage = null) { @@ -387,10 +409,10 @@ export default class ExpressionEditorTextfield extends React.Component { }, }, { - name: "tab", - bindKey: { win: "Tab", mac: "Tab" }, + name: "chooseSuggestion", + bindKey: null, exec: () => { - this.handleTab(); + this.chooseSuggestion(); }, }, ]; diff --git a/frontend/test/metabase/scenarios/custom-column/custom-column.cy.spec.js b/frontend/test/metabase/scenarios/custom-column/custom-column.cy.spec.js index dd582d755117b27ae66ab4329c027ea6396bf83d..13b53a0f27936512d667f86d07564cdd40f2fc3a 100644 --- a/frontend/test/metabase/scenarios/custom-column/custom-column.cy.spec.js +++ b/frontend/test/metabase/scenarios/custom-column/custom-column.cy.spec.js @@ -462,4 +462,68 @@ describe("scenarios > question > custom column", () => { cy.findByText("MiscDate Previous 30 Years"); // Filter name cy.findByText("MiscDate"); // Column name }); + + it("should allow switching focus with Tab", () => { + openOrdersTable({ mode: "notebook" }); + cy.icon("add_data").click(); + + enterCustomColumnDetails({ formula: "1 + 2" }); + + // next focus: a link + cy.realPress("Tab"); + cy.focused() + .should("have.attr", "class") + .and("eq", "link"); + cy.focused() + .should("have.attr", "target") + .and("eq", "_blank"); + + // next focus: the textbox for the name + cy.realPress("Tab"); + cy.focused() + .should("have.attr", "value") + .and("eq", ""); + cy.focused() + .should("have.attr", "placeholder") + .and("eq", "Something nice and descriptive"); + + // Shift+Tab twice and we're back at the editor + cy.realPress(["Shift", "Tab"]); + cy.realPress(["Shift", "Tab"]); + cy.focused() + .should("have.attr", "class") + .and("eq", "ace_text-input"); + }); + + it("should allow choosing a suggestion with Tab", () => { + openOrdersTable({ mode: "notebook" }); + cy.icon("add_data").click(); + + enterCustomColumnDetails({ formula: "[" }); + + // Suggestion popover shows up and this select the first one ([Created At]) + cy.realPress("Tab"); + + // Focus remains on the expression editor + cy.focused() + .should("have.attr", "class") + .and("eq", "ace_text-input"); + + // Tab twice to focus on the name box + cy.realPress("Tab"); + cy.realPress("Tab"); + cy.focused() + .should("have.attr", "value") + .and("eq", ""); + cy.focused() + .should("have.attr", "placeholder") + .and("eq", "Something nice and descriptive"); + + // Shift+Tab twice and we're back at the editor + cy.realPress(["Shift", "Tab"]); + cy.realPress(["Shift", "Tab"]); + cy.focused() + .should("have.attr", "class") + .and("eq", "ace_text-input"); + }); }); diff --git a/frontend/test/metabase/scenarios/question/filter.cy.spec.js b/frontend/test/metabase/scenarios/question/filter.cy.spec.js index 391a1a78eebdf2e2bb4a9fb87f7f93385509b61d..a50701d9f4588cdcc4d74af77f693f4044f46460 100644 --- a/frontend/test/metabase/scenarios/question/filter.cy.spec.js +++ b/frontend/test/metabase/scenarios/question/filter.cy.spec.js @@ -818,6 +818,45 @@ describe("scenarios > question > filter", () => { cy.findByText("Expecting field but found 0"); }); + it("should allow switching focus with Tab", () => { + openOrdersTable({ mode: "notebook" }); + cy.findByText("Filter").click(); + cy.findByText("Custom Expression").click(); + cy.get(".ace_text-input").type("[Tax] > 0"); + + // Tab switches the focus to the "Done" button + cy.realPress("Tab"); + cy.focused() + .should("have.attr", "class") + .and("contains", "Button"); + }); + + it("should allow choosing a suggestion with Tab", () => { + openOrdersTable({ mode: "notebook" }); + cy.findByText("Filter").click(); + cy.findByText("Custom Expression").click(); + + // Try to auto-complete Tax + cy.get(".ace_text-input").type("Ta"); + + // Suggestion popover shows up and this select the first one ([Created At]) + cy.realPress("Tab"); + + // Focus remains on the expression editor + cy.focused() + .should("have.attr", "class") + .and("eq", "ace_text-input"); + + // Finish to complete a valid expression, i.e. [Tax] > 42 + cy.get(".ace_text-input").type("> 42"); + + // Tab switches the focus to the "Done" button + cy.realPress("Tab"); + cy.focused() + .should("have.attr", "class") + .and("contains", "Button"); + }); + it.skip("should work on twice summarized questions (metabase#15620)", () => { visitQuestionAdhoc({ dataset_query: {