[Task 3061766] Global Keyboard Shortcuts, implemented through the Command Bar (#1789)

* keyboard shortcuts using tinykeys

* refmt and fix lints

* retarget keyboard shortcuts to the body instead of the root element of the React component tree

* refmt

* Update src/Explorer/Menus/CommandBar/CommandBarUtil.tsx

Co-authored-by: Laurent Nguyen <laurent.nguyen@microsoft.com>

* add Save binding to New Item command bar

---------

Co-authored-by: Laurent Nguyen <laurent.nguyen@microsoft.com>
This commit is contained in:
Ashley Stanton-Nurse
2024-04-17 11:19:09 -07:00
committed by GitHub
parent e0cb3da6aa
commit a44ed1f45c
13 changed files with 206 additions and 45 deletions

View File

@@ -1,6 +1,7 @@
/**
* React component for Command button component.
*/
import { KeyboardAction } from "KeyboardShortcuts";
import * as React from "react";
import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
import { KeyCodes } from "../../../Common/Constants";
@@ -30,7 +31,7 @@ export interface CommandButtonComponentProps {
/**
* Click handler for command button click
*/
onCommandClick: (e: React.SyntheticEvent) => void;
onCommandClick: (e: React.SyntheticEvent | KeyboardEvent) => void;
/**
* Label for the button
@@ -107,10 +108,17 @@ export interface CommandButtonComponentProps {
* Vertical bar to divide buttons
*/
isDivider?: boolean;
/**
* Aria-label for the button
*/
ariaLabel: string;
/**
* If specified, a keyboard action that should trigger this button's onCommandClick handler when activated.
* If not specified, the button will not be triggerable by keyboard shortcuts.
*/
keyboardAction?: KeyboardAction;
}
export class CommandButtonComponent extends React.Component<CommandButtonComponentProps> {

View File

@@ -5,6 +5,7 @@
*/
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import { useKeyboardActionHandlers } from "KeyboardShortcuts";
import { userContext } from "UserContext";
import * as React from "react";
import create, { UseStore } from "zustand";
@@ -40,6 +41,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const buttons = useCommandBar((state) => state.contextButtons);
const isHidden = useCommandBar((state) => state.isHidden);
const backgroundColor = StyleConstants.BaseLight;
const setKeyboardShortcutHandlers = useKeyboardActionHandlers((state) => state.setHandlers);
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
const buttons =
@@ -105,6 +107,10 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
},
};
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons);
setKeyboardShortcutHandlers(keyboardHandlers);
return (
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
<FluentCommandBar

View File

@@ -1,3 +1,4 @@
import { KeyboardAction } from "KeyboardShortcuts";
import { ReactTabKind, useTabs } from "hooks/useTabs";
import * as React from "react";
import AddCollectionIcon from "../../../../images/AddCollection.svg";
@@ -297,6 +298,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
id: "newQueryBtn",
iconSrc: AddSqlQueryIcon,
iconAlt: label,
keyboardAction: KeyboardAction.NEW_QUERY,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
@@ -312,6 +314,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
id: "newQueryBtn",
iconSrc: AddSqlQueryIcon,
iconAlt: label,
keyboardAction: KeyboardAction.NEW_QUERY,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection);
@@ -397,6 +400,7 @@ function createOpenQueryButton(container: Explorer): CommandButtonComponentProps
return {
iconSrc: BrowseQueriesIcon,
iconAlt: label,
keyboardAction: KeyboardAction.OPEN_QUERY,
onCommandClick: () =>
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />),
commandButtonLabel: label,
@@ -411,6 +415,7 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
return {
iconSrc: OpenQueryFromDiskIcon,
iconAlt: label,
keyboardAction: KeyboardAction.OPEN_QUERY_FROM_DISK,
onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", <LoadQueryPane />),
commandButtonLabel: label,
ariaLabel: label,

View File

@@ -7,6 +7,7 @@ import {
IDropdownStyles,
} from "@fluentui/react";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { KeyboardHandlerMap } from "KeyboardShortcuts";
import * as React from "react";
import _ from "underscore";
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
@@ -233,3 +234,28 @@ export const createConnectionStatus = (container: Explorer, poolId: PoolIdType,
onRender: () => <ConnectionStatus container={container} poolId={poolId} />,
};
};
export function createKeyboardHandlers(allButtons: CommandButtonComponentProps[]): KeyboardHandlerMap {
const handlers: KeyboardHandlerMap = {};
function createHandlers(buttons: CommandButtonComponentProps[]) {
buttons.forEach((button) => {
if (!button.disabled && button.keyboardAction) {
handlers[button.keyboardAction] = (e) => {
button.onCommandClick(e);
// If the handler is bound, it means the button is visible and enabled, so we should prevent the default action
return true;
};
}
if (button.children && button.children.length > 0) {
createHandlers(button.children);
}
});
}
createHandlers(allButtons);
return handlers;
}

View File

@@ -1,6 +1,7 @@
import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { Platform, configContext } from "ConfigContext";
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { KeyboardAction } from "KeyboardShortcuts";
import { QueryConstants } from "Shared/Constants";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import * as ko from "knockout";
@@ -907,6 +908,7 @@ export default class DocumentsTab extends TabsBase {
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onSaveNewDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
@@ -936,6 +938,7 @@ export default class DocumentsTab extends TabsBase {
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onSaveExistingDocumentClick,
commandButtonLabel: label,
ariaLabel: label,

View File

@@ -10,6 +10,7 @@ import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilo
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
import { useSelectedNode } from "Explorer/useSelectedNode";
import { KeyboardAction } from "KeyboardShortcuts";
import { QueryConstants } from "Shared/Constants";
import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
import { Action } from "Shared/Telemetry/TelemetryConstants";
@@ -393,6 +394,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
buttons.push({
iconSrc: ExecuteQueryIcon,
iconAlt: label,
keyboardAction: KeyboardAction.EXECUTE_ITEM,
onCommandClick: this.props.isSampleCopilotActive
? () => OnExecuteQueryClick(this.props.copilotStore)
: this.onExecuteQueryClick,
@@ -408,6 +410,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
buttons.push({
iconSrc: SaveQueryIcon,
iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onSaveQueryClick,
commandButtonLabel: label,
ariaLabel: label,
@@ -468,6 +471,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
buttons.push({
iconSrc: CancelQueryIcon,
iconAlt: label,
keyboardAction: KeyboardAction.CANCEL_QUERY,
onCommandClick: () => this.queryAbortController.abort(),
commandButtonLabel: label,
ariaLabel: label,

View File

@@ -1,5 +1,6 @@
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
import { Pivot, PivotItem } from "@fluentui/react";
import { KeyboardAction } from "KeyboardShortcuts";
import React from "react";
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
import DiscardIcon from "../../../../images/discard.svg";
@@ -321,6 +322,7 @@ export default class StoredProcedureTabComponent extends React.Component<
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onSaveClick,
commandButtonLabel: label,
ariaLabel: label,
@@ -334,6 +336,7 @@ export default class StoredProcedureTabComponent extends React.Component<
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onUpdateClick,
commandButtonLabel: label,
ariaLabel: label,
@@ -360,6 +363,7 @@ export default class StoredProcedureTabComponent extends React.Component<
buttons.push({
iconSrc: ExecuteQueryIcon,
iconAlt: label,
keyboardAction: KeyboardAction.EXECUTE_ITEM,
onCommandClick: () => {
this.collection.container.openExecuteSprocParamsPanel(this.node);
},

View File

@@ -1,5 +1,6 @@
import { TriggerDefinition } from "@azure/cosmos";
import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react";
import { KeyboardAction } from "KeyboardShortcuts";
import React, { Component } from "react";
import DiscardIcon from "../../../images/discard.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
@@ -227,6 +228,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
...this,
iconSrc: SaveIcon,
iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onSaveClick,
commandButtonLabel: label,
ariaLabel: label,
@@ -241,6 +243,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
...this,
iconSrc: SaveIcon,
iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onUpdateClick,
commandButtonLabel: label,
ariaLabel: label,

View File

@@ -1,12 +1,13 @@
import { UserDefinedFunctionDefinition } from "@azure/cosmos";
import { Label, TextField } from "@fluentui/react";
import { KeyboardAction } from "KeyboardShortcuts";
import React, { Component } from "react";
import DiscardIcon from "../../../images/discard.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
import * as Constants from "../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { createUserDefinedFunction } from "../../Common/dataAccess/createUserDefinedFunction";
import { updateUserDefinedFunction } from "../../Common/dataAccess/updateUserDefinedFunction";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
@@ -80,6 +81,7 @@ export default class UserDefinedFunctionTabContent extends Component<
setState: this.setState,
iconSrc: SaveIcon,
iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onSaveClick,
commandButtonLabel: label,
ariaLabel: label,
@@ -94,6 +96,7 @@ export default class UserDefinedFunctionTabContent extends Component<
...this,
iconSrc: SaveIcon,
iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onUpdateClick,
commandButtonLabel: label,
ariaLabel: label,