feat: Help menu displaying keyboard shortcuts (#689)
Help menu that shows icon and keyboard shortcuts for registered actions. Refactored keyboard management to only allow one handler per key event/key combo [AB#17122]
Esse commit está contido em:
@@ -121,3 +121,15 @@
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.app-help-menu-icon {
|
||||
padding: 6px 10px;
|
||||
color: #ccc;
|
||||
display: inline-block;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: $lighter-2;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -13,8 +13,8 @@ import { ErrorHandler } from "./react/components/common/errorHandler/errorHandle
|
||||
import { KeyboardManager } from "./react/components/common/keyboardManager/keyboardManager";
|
||||
import { TitleBar } from "./react/components/shell/titleBar";
|
||||
import { StatusBar } from "./react/components/shell/statusBar";
|
||||
import { strings } from "./common/strings";
|
||||
import { StatusBarMetrics } from "./react/components/shell/statusBarMetrics";
|
||||
import { HelpMenu } from "./react/components/shell/helpMenu";
|
||||
|
||||
interface IAppProps {
|
||||
currentProject?: IProject;
|
||||
@@ -77,6 +77,7 @@ export default class App extends React.Component<IAppProps> {
|
||||
<div className={`app-shell platform-${platform}`}>
|
||||
<TitleBar icon="fas fa-tags"
|
||||
title={this.props.currentProject ? this.props.currentProject.name : ""}>
|
||||
<div className="app-help-menu-icon"><HelpMenu/></div>
|
||||
</TitleBar>
|
||||
<div className="app-main">
|
||||
<Sidebar project={this.props.currentProject} />
|
||||
|
||||
@@ -15,6 +15,13 @@ export const english: IAppStrings = {
|
||||
provider: "Provider",
|
||||
homePage: "Home Page",
|
||||
},
|
||||
titleBar: {
|
||||
help: "Help",
|
||||
minimize: "Minimize",
|
||||
maximize: "Maximize",
|
||||
restore: "Restore",
|
||||
close: "Close",
|
||||
},
|
||||
homePage: {
|
||||
newProject: "New Project",
|
||||
openLocalProject: {
|
||||
@@ -167,17 +174,17 @@ export const english: IAppStrings = {
|
||||
toolbar: {
|
||||
select: "Select (V)",
|
||||
pan: "Pan",
|
||||
drawRectangle: "Draw Rectangle (R)",
|
||||
drawPolygon: "Draw Polygon (P)",
|
||||
copyRectangle: "Copy Rectangle (Ctrl + W)",
|
||||
copy: "Copy Regions (Ctrl + C)",
|
||||
cut: "Cut Regions (Ctrl + X)",
|
||||
paste: "Paste Regions (Ctrl + V)",
|
||||
removeAllRegions: "Remove All Regions (Ctrl + Delete)",
|
||||
previousAsset: "Previous Asset (W)",
|
||||
nextAsset: "Next Asset (S)",
|
||||
saveProject: "Save Project (Ctrl + S)",
|
||||
exportProject: "Export Project (Ctrl + E)",
|
||||
drawRectangle: "Draw Rectangle",
|
||||
drawPolygon: "Draw Polygon",
|
||||
copyRectangle: "Copy Rectangle",
|
||||
copy: "Copy Regions",
|
||||
cut: "Cut Regions",
|
||||
paste: "Paste Regions",
|
||||
removeAllRegions: "Remove All Regions",
|
||||
previousAsset: "Previous Asset",
|
||||
nextAsset: "Next Asset",
|
||||
saveProject: "Save Project",
|
||||
exportProject: "Export Project",
|
||||
},
|
||||
videoPlayer: {
|
||||
previousTaggedFrame: {
|
||||
@@ -193,7 +200,16 @@ export const english: IAppStrings = {
|
||||
tooltip: "Next Frame",
|
||||
},
|
||||
},
|
||||
help: {
|
||||
title: "Toggle Help Menu",
|
||||
escape: "Escape Help Menu",
|
||||
},
|
||||
assetError: "Unable to load asset",
|
||||
tags: {
|
||||
hotKey: {
|
||||
help: "Apply Tag with Hot Key",
|
||||
},
|
||||
},
|
||||
canvas: {
|
||||
removeAllRegions: {
|
||||
title: "Remove All Regions",
|
||||
|
||||
@@ -15,6 +15,13 @@ export const spanish: IAppStrings = {
|
||||
provider: "Proveedor",
|
||||
homePage: "Página de Inicio",
|
||||
},
|
||||
titleBar: {
|
||||
help: "Ayuda",
|
||||
minimize: "Minimizar",
|
||||
maximize: "Maximizar",
|
||||
restore: "Restaurar",
|
||||
close: "Cerrar",
|
||||
},
|
||||
homePage: {
|
||||
newProject: "Nuevo Proyecto",
|
||||
recentProjects: "Proyectos Recientes",
|
||||
@@ -168,17 +175,17 @@ export const spanish: IAppStrings = {
|
||||
toolbar: {
|
||||
select: "Seleccionar",
|
||||
pan: "Pan",
|
||||
drawRectangle: "Dibujar Rectángulo (R)",
|
||||
drawPolygon: "Dibujar Polígono (P)",
|
||||
copyRectangle: "Copia rectángulo (Ctrl + W)",
|
||||
copy: "Copiar regiones (Ctrl + C)",
|
||||
cut: "Cortar regiones (Ctrl + X)",
|
||||
paste: "Pegar regiones (Ctrl + V)",
|
||||
removeAllRegions: "Eliminar Todas Las Regiones (Ctrl + Delete)",
|
||||
previousAsset: "Activo anterior (W)",
|
||||
nextAsset: "Siguiente activo (S)",
|
||||
saveProject: "Guardar Proyecto (Ctrl + S)",
|
||||
exportProject: "Exprtar Proyecto (Ctrl + E)",
|
||||
drawRectangle: "Dibujar Rectángulo",
|
||||
drawPolygon: "Dibujar Polígono",
|
||||
copyRectangle: "Copia rectángulo",
|
||||
copy: "Copiar regiones",
|
||||
cut: "Cortar regiones",
|
||||
paste: "Pegar regiones",
|
||||
removeAllRegions: "Eliminar Todas Las Regiones",
|
||||
previousAsset: "Activo anterior",
|
||||
nextAsset: "Siguiente activo",
|
||||
saveProject: "Guardar Proyecto",
|
||||
exportProject: "Exprtar Proyecto",
|
||||
},
|
||||
videoPlayer: {
|
||||
previousTaggedFrame: {
|
||||
@@ -194,7 +201,16 @@ export const spanish: IAppStrings = {
|
||||
tooltip: "Siguiente marco",
|
||||
},
|
||||
},
|
||||
help: {
|
||||
title: "Abrir/cerrar el menú de ayuda",
|
||||
escape: "Escapar el menú de ayuda",
|
||||
},
|
||||
assetError: "No se puede mostrar el activo",
|
||||
tags: {
|
||||
hotKey: {
|
||||
help: "Aplicar etiqueta con tecla de acceso rápido",
|
||||
},
|
||||
},
|
||||
canvas: {
|
||||
removeAllRegions: {
|
||||
title: "Borrar Regiones",
|
||||
|
||||
@@ -29,6 +29,9 @@ import { RegionDataType, RegionData } from "vott-ct/lib/js/CanvasTools/Core/Regi
|
||||
import { randomIntInRange } from "./utils";
|
||||
import { appInfo } from "./appInfo";
|
||||
import { SelectionMode } from "vott-ct/lib/js/CanvasTools/Interface/ISelectorSettings";
|
||||
import { IKeyboardBindingProps } from "../react/components/common/keyboardBinding/keyboardBinding";
|
||||
import { KeyEventType } from "../react/components/common/keyboardManager/keyboardManager";
|
||||
import { IKeyboardRegistrations } from "../react/components/common/keyboardManager/keyboardRegistrationManager";
|
||||
|
||||
export default class MockFactory {
|
||||
|
||||
@@ -942,6 +945,32 @@ export default class MockFactory {
|
||||
};
|
||||
}
|
||||
|
||||
public static createKeyboardRegistrations(count = 5, handlers?): IKeyboardRegistrations {
|
||||
const keyDownRegs = {};
|
||||
if (!handlers) {
|
||||
handlers = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
handlers.push(jest.fn(() => i));
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < count; i++) {
|
||||
const upper = String.fromCharCode(65 + i);
|
||||
const lower = String.fromCharCode(97 + i);
|
||||
const binding: IKeyboardBindingProps = {
|
||||
displayName: `Binding ${i + 1}`,
|
||||
accelerators: [upper, lower],
|
||||
handler: handlers[i],
|
||||
icon: `test-icon-${i + 1}`,
|
||||
keyEventType: KeyEventType.KeyDown,
|
||||
};
|
||||
keyDownRegs[upper] = binding;
|
||||
keyDownRegs[lower] = binding;
|
||||
}
|
||||
return {
|
||||
keydown: keyDownRegs,
|
||||
};
|
||||
}
|
||||
|
||||
private static pageProps(projectId: string, method: string) {
|
||||
return {
|
||||
project: null,
|
||||
|
||||
@@ -18,6 +18,13 @@ export interface IAppStrings {
|
||||
provider: string;
|
||||
homePage: string;
|
||||
};
|
||||
titleBar: {
|
||||
help: string;
|
||||
minimize: string;
|
||||
maximize: string;
|
||||
restore: string;
|
||||
close: string;
|
||||
};
|
||||
homePage: {
|
||||
newProject: string;
|
||||
openLocalProject: {
|
||||
@@ -195,7 +202,16 @@ export interface IAppStrings {
|
||||
tooltip: string,
|
||||
},
|
||||
}
|
||||
help: {
|
||||
title: string;
|
||||
escape: string;
|
||||
}
|
||||
assetError: string;
|
||||
tags: {
|
||||
hotKey: {
|
||||
help: string;
|
||||
},
|
||||
}
|
||||
canvas: {
|
||||
removeAllRegions: {
|
||||
title: string;
|
||||
|
||||
@@ -38,16 +38,17 @@ export enum ErrorCode {
|
||||
// the enum key is in Pascal casing
|
||||
Unknown = "unknown",
|
||||
GenericRenderError = "genericRenderError",
|
||||
CanvasError = "canvasError",
|
||||
V1ImportError = "v1ImportError",
|
||||
ProjectUploadError = "projectUploadError",
|
||||
ProjectDeleteError = "projectDeleteError",
|
||||
ProjectInvalidJson = "projectInvalidJson",
|
||||
ProjectInvalidSecurityToken = "projectInvalidSecurityToken",
|
||||
ProjectDuplicateName = "projectDuplicateName",
|
||||
ProjectUploadError = "projectUploadError",
|
||||
ProjectDeleteError = "projectDeleteError",
|
||||
SecurityTokenNotFound = "securityTokenNotFound",
|
||||
ExportFormatNotFound = "exportFormatNotFound",
|
||||
CanvasError = "canvasError",
|
||||
V1ImportError = "v1ImportError",
|
||||
PasteRegionTooBigError = "pasteRegionTooBigError",
|
||||
PasteRegionTooBig = "pasteRegionTooBig",
|
||||
OverloadedKeyBinding = "overloadedKeyBinding",
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -79,13 +79,17 @@ export class VideoAsset extends React.Component<IVideoAssetProps> {
|
||||
<CustomVideoPlayerButton order={1.1}
|
||||
accelerators={["ArrowLeft", "a", "A"]}
|
||||
tooltip={strings.editorPage.videoPlayer.previousExpectedFrame.tooltip}
|
||||
onClick={this.movePreviousExpectedFrame}>
|
||||
onClick={this.movePreviousExpectedFrame}
|
||||
icon={"fa-caret-left fa-lg"}
|
||||
>
|
||||
<i className="fas fa-caret-left fa-lg" />
|
||||
</CustomVideoPlayerButton>
|
||||
<CustomVideoPlayerButton order={1.2}
|
||||
accelerators={["ArrowRight", "d", "D"]}
|
||||
tooltip={strings.editorPage.videoPlayer.nextExpectedFrame.tooltip}
|
||||
onClick={this.moveNextExpectedFrame}>
|
||||
onClick={this.moveNextExpectedFrame}
|
||||
icon={"fa-caret-right fa-lg"}
|
||||
>
|
||||
<i className="fas fa-caret-right fa-lg" />
|
||||
</CustomVideoPlayerButton>
|
||||
<CurrentTimeDisplay order={1.3} />
|
||||
@@ -95,13 +99,17 @@ export class VideoAsset extends React.Component<IVideoAssetProps> {
|
||||
<CustomVideoPlayerButton order={8.1}
|
||||
accelerators={["q", "Q"]}
|
||||
tooltip={strings.editorPage.videoPlayer.previousTaggedFrame.tooltip}
|
||||
onClick={this.movePreviousTaggedFrame}>
|
||||
onClick={this.movePreviousTaggedFrame}
|
||||
icon={"fas fa-step-backward"}
|
||||
>
|
||||
<i className="fas fa-step-backward"></i>
|
||||
</CustomVideoPlayerButton>
|
||||
<CustomVideoPlayerButton order={8.2}
|
||||
accelerators={["e", "E"]}
|
||||
tooltip={strings.editorPage.videoPlayer.nextTaggedFrame.tooltip}
|
||||
onClick={this.moveNextTaggedFrame}>
|
||||
onClick={this.moveNextTaggedFrame}
|
||||
icon={"fa-step-forward"}
|
||||
>
|
||||
<i className="fas fa-step-forward"></i>
|
||||
</CustomVideoPlayerButton>
|
||||
</ControlBar>
|
||||
|
||||
@@ -13,13 +13,14 @@ describe("Keyboard Binding Component", () => {
|
||||
|
||||
const accelerators = ["Ctrl+1"];
|
||||
const defaultProps: IKeyboardBindingProps = {
|
||||
displayName: "Keyboard binding",
|
||||
keyEventType: KeyEventType.KeyDown,
|
||||
accelerators,
|
||||
onKeyEvent: onKeyDownHandler,
|
||||
handler: onKeyDownHandler,
|
||||
};
|
||||
|
||||
const registrationMock = KeyboardRegistrationManager as jest.Mocked<typeof KeyboardRegistrationManager>;
|
||||
registrationMock.prototype.addHandler = jest.fn(() => deregisterFunc);
|
||||
registrationMock.prototype.registerBinding = jest.fn(() => deregisterFunc);
|
||||
|
||||
function createComponent(props?: IKeyboardBindingProps): ReactWrapper {
|
||||
props = props || defaultProps;
|
||||
@@ -43,8 +44,13 @@ describe("Keyboard Binding Component", () => {
|
||||
|
||||
it("registered the keydown key code and event handler", () => {
|
||||
wrapper = createComponent();
|
||||
expect(registrationMock.prototype.addHandler).toBeCalledWith(
|
||||
KeyEventType.KeyDown, defaultProps.accelerators, defaultProps.onKeyEvent);
|
||||
const expectedBindingProps: IKeyboardBindingProps = {
|
||||
accelerators: defaultProps.accelerators,
|
||||
keyEventType: KeyEventType.KeyDown,
|
||||
handler: defaultProps.handler,
|
||||
displayName: expect.any(String),
|
||||
};
|
||||
expect(registrationMock.prototype.registerBinding).toBeCalledWith(expectedBindingProps);
|
||||
});
|
||||
|
||||
it("registered the keyup key code and event handler", () => {
|
||||
@@ -52,8 +58,13 @@ describe("Keyboard Binding Component", () => {
|
||||
...defaultProps,
|
||||
keyEventType: KeyEventType.KeyUp,
|
||||
});
|
||||
expect(registrationMock.prototype.addHandler).toBeCalledWith(
|
||||
KeyEventType.KeyUp, defaultProps.accelerators, defaultProps.onKeyEvent);
|
||||
const expectedBindingProps: IKeyboardBindingProps = {
|
||||
accelerators: defaultProps.accelerators,
|
||||
keyEventType: KeyEventType.KeyUp,
|
||||
handler: defaultProps.handler,
|
||||
displayName: expect.any(String),
|
||||
};
|
||||
expect(registrationMock.prototype.registerBinding).toBeCalledWith(expectedBindingProps);
|
||||
});
|
||||
|
||||
it("registered the keypress key code and event handler", () => {
|
||||
@@ -61,8 +72,13 @@ describe("Keyboard Binding Component", () => {
|
||||
...defaultProps,
|
||||
keyEventType: KeyEventType.KeyPress,
|
||||
});
|
||||
expect(registrationMock.prototype.addHandler).toBeCalledWith(
|
||||
KeyEventType.KeyPress, defaultProps.accelerators, defaultProps.onKeyEvent);
|
||||
const expectedBindingProps: IKeyboardBindingProps = {
|
||||
accelerators: defaultProps.accelerators,
|
||||
keyEventType: KeyEventType.KeyPress,
|
||||
handler: defaultProps.handler,
|
||||
displayName: expect.any(String),
|
||||
};
|
||||
expect(registrationMock.prototype.registerBinding).toBeCalledWith(expectedBindingProps);
|
||||
});
|
||||
|
||||
it("deregisters the event handler", () => {
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import { KeyboardContext, IKeyboardContext, KeyEventType } from "../keyboardManager/keyboardManager";
|
||||
import React from "react";
|
||||
|
||||
/**
|
||||
* Properties needed for a keyboard binding
|
||||
*/
|
||||
export interface IKeyboardBindingProps {
|
||||
/** Keys that the action is bound to */
|
||||
accelerators: string[];
|
||||
onKeyEvent: (evt?: KeyboardEvent) => void;
|
||||
/** Friendly name for keyboard binding for display in help menu */
|
||||
displayName: string;
|
||||
/** Action to trigger upon key event */
|
||||
handler: (evt?: KeyboardEvent) => void;
|
||||
/** Type of key event (keypress, keyup, keydown) */
|
||||
keyEventType?: KeyEventType;
|
||||
/** Icon to display in help menu */
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export class KeyboardBinding extends React.Component<IKeyboardBindingProps> {
|
||||
@@ -14,11 +24,7 @@ export class KeyboardBinding extends React.Component<IKeyboardBindingProps> {
|
||||
|
||||
public componentDidMount() {
|
||||
if (this.context && this.context.keyboard) {
|
||||
this.deregisterBinding = this.context.keyboard.addHandler(
|
||||
this.props.keyEventType || KeyEventType.KeyDown,
|
||||
this.props.accelerators,
|
||||
this.props.onKeyEvent,
|
||||
);
|
||||
this.deregisterBinding = this.context.keyboard.registerBinding(this.props);
|
||||
} else {
|
||||
console.warn("Keyboard Mananger context cannot be found - Keyboard binding has NOT been set.");
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ describe("Keyboard Manager Component", () => {
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
(registrationManagerMock.prototype.invokeHandlers as any).mockClear();
|
||||
(registrationManagerMock.prototype.invokeHandler as any).mockClear();
|
||||
addEventListenerSpy = jest.spyOn(window, "addEventListener");
|
||||
removeEventListenerSpy = jest.spyOn(window, "removeEventListener");
|
||||
wrapper = createComponent();
|
||||
@@ -53,7 +53,7 @@ describe("Keyboard Manager Component", () => {
|
||||
|
||||
window.dispatchEvent(keyboardEvent);
|
||||
|
||||
expect(registrationManagerMock.prototype.invokeHandlers)
|
||||
expect(registrationManagerMock.prototype.invokeHandler)
|
||||
.toBeCalledWith(KeyEventType.KeyDown, "Ctrl+1", keyboardEvent);
|
||||
});
|
||||
|
||||
@@ -66,7 +66,7 @@ describe("Keyboard Manager Component", () => {
|
||||
|
||||
window.dispatchEvent(keyboardEvent);
|
||||
|
||||
expect(registrationManagerMock.prototype.invokeHandlers)
|
||||
expect(registrationManagerMock.prototype.invokeHandler)
|
||||
.toBeCalledWith(KeyEventType.KeyUp, "Ctrl+1", keyboardEvent);
|
||||
});
|
||||
|
||||
@@ -79,7 +79,7 @@ describe("Keyboard Manager Component", () => {
|
||||
|
||||
window.dispatchEvent(keyboardEvent);
|
||||
|
||||
expect(registrationManagerMock.prototype.invokeHandlers)
|
||||
expect(registrationManagerMock.prototype.invokeHandler)
|
||||
.toBeCalledWith(KeyEventType.KeyPress, "Ctrl+1", keyboardEvent);
|
||||
});
|
||||
|
||||
@@ -93,7 +93,7 @@ describe("Keyboard Manager Component", () => {
|
||||
|
||||
window.dispatchEvent(keyboardEvent);
|
||||
|
||||
expect(registrationManagerMock.prototype.invokeHandlers)
|
||||
expect(registrationManagerMock.prototype.invokeHandler)
|
||||
.toBeCalledWith(KeyEventType.KeyDown, "Alt+1", keyboardEvent);
|
||||
});
|
||||
|
||||
@@ -107,7 +107,7 @@ describe("Keyboard Manager Component", () => {
|
||||
|
||||
window.dispatchEvent(keyboardEvent);
|
||||
|
||||
expect(registrationManagerMock.prototype.invokeHandlers)
|
||||
expect(registrationManagerMock.prototype.invokeHandler)
|
||||
.toBeCalledWith(KeyEventType.KeyUp, "Alt+1", keyboardEvent);
|
||||
});
|
||||
|
||||
@@ -121,7 +121,7 @@ describe("Keyboard Manager Component", () => {
|
||||
|
||||
window.dispatchEvent(keyboardEvent);
|
||||
|
||||
expect(registrationManagerMock.prototype.invokeHandlers)
|
||||
expect(registrationManagerMock.prototype.invokeHandler)
|
||||
.toBeCalledWith(KeyEventType.KeyPress, "Alt+1", keyboardEvent);
|
||||
});
|
||||
|
||||
@@ -135,7 +135,7 @@ describe("Keyboard Manager Component", () => {
|
||||
|
||||
window.dispatchEvent(keyboardEvent);
|
||||
|
||||
expect(registrationManagerMock.prototype.invokeHandlers)
|
||||
expect(registrationManagerMock.prototype.invokeHandler)
|
||||
.toBeCalledWith(KeyEventType.KeyDown, "F1", keyboardEvent);
|
||||
});
|
||||
|
||||
@@ -149,7 +149,7 @@ describe("Keyboard Manager Component", () => {
|
||||
|
||||
window.dispatchEvent(keyboardEvent);
|
||||
|
||||
expect(registrationManagerMock.prototype.invokeHandlers)
|
||||
expect(registrationManagerMock.prototype.invokeHandler)
|
||||
.toBeCalledWith(KeyEventType.KeyUp, "F1", keyboardEvent);
|
||||
});
|
||||
|
||||
@@ -163,7 +163,7 @@ describe("Keyboard Manager Component", () => {
|
||||
|
||||
window.dispatchEvent(keyboardEvent);
|
||||
|
||||
expect(registrationManagerMock.prototype.invokeHandlers)
|
||||
expect(registrationManagerMock.prototype.invokeHandler)
|
||||
.toBeCalledWith(KeyEventType.KeyPress, "F1", keyboardEvent);
|
||||
});
|
||||
|
||||
@@ -183,7 +183,7 @@ describe("Keyboard Manager Component", () => {
|
||||
|
||||
window.dispatchEvent(keyboardEvent);
|
||||
|
||||
expect(registrationManagerMock.prototype.invokeHandlers).not.toBeCalled();
|
||||
expect(registrationManagerMock.prototype.invokeHandler).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("ignores keyboard events when UI is focused on an textarea element", () => {
|
||||
@@ -201,7 +201,7 @@ describe("Keyboard Manager Component", () => {
|
||||
|
||||
window.dispatchEvent(keyboardEvent);
|
||||
|
||||
expect(registrationManagerMock.prototype.invokeHandlers).not.toBeCalled();
|
||||
expect(registrationManagerMock.prototype.invokeHandler).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("ignores keyboard events when UI is focused on select elements", () => {
|
||||
@@ -219,7 +219,7 @@ describe("Keyboard Manager Component", () => {
|
||||
|
||||
window.dispatchEvent(keyboardEvent);
|
||||
|
||||
expect(registrationManagerMock.prototype.invokeHandlers).not.toBeCalled();
|
||||
expect(registrationManagerMock.prototype.invokeHandler).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("does not ignore keyboard events when UI is focused on other form elements", () => {
|
||||
@@ -237,7 +237,7 @@ describe("Keyboard Manager Component", () => {
|
||||
|
||||
window.dispatchEvent(keyboardEvent);
|
||||
|
||||
expect(registrationManagerMock.prototype.invokeHandlers).toBeCalled();
|
||||
expect(registrationManagerMock.prototype.invokeHandler).toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,7 +63,7 @@ export class KeyboardManager extends React.Component<any, IKeyboardContext> {
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.keyboard.invokeHandlers(evt.type as KeyEventType, this.getKeyParts(evt), evt);
|
||||
this.state.keyboard.invokeHandler(evt.type as KeyEventType, this.getKeyParts(evt), evt);
|
||||
}
|
||||
|
||||
private isDisabled(): boolean {
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import { KeyboardRegistrationManager } from "./keyboardRegistrationManager";
|
||||
import { KeyEventType } from "./keyboardManager";
|
||||
import { IKeyboardBindingProps } from "../keyboardBinding/keyboardBinding";
|
||||
|
||||
describe("Keyboard Registration Manager", () => {
|
||||
let keyboardManager: KeyboardRegistrationManager = null;
|
||||
|
||||
function addHandler(keyboardManager: KeyboardRegistrationManager,
|
||||
keyEventType: KeyEventType, accelerators: string[], handler) {
|
||||
const bindingProps: IKeyboardBindingProps = {
|
||||
accelerators,
|
||||
displayName: "test binding",
|
||||
handler,
|
||||
keyEventType,
|
||||
};
|
||||
return keyboardManager.registerBinding(bindingProps);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
keyboardManager = new KeyboardRegistrationManager();
|
||||
});
|
||||
@@ -13,119 +25,70 @@ describe("Keyboard Registration Manager", () => {
|
||||
expect(keyboardManager).not.toBeNull();
|
||||
});
|
||||
|
||||
it("can add keybard event handlers", () => {
|
||||
it("can add keyboard event handlers", () => {
|
||||
const keyCode1 = "Ctrl+1";
|
||||
const handler1 = (evt: KeyboardEvent) => null;
|
||||
const keyCode2 = "Ctrl+S";
|
||||
const handler2 = (evt: KeyboardEvent) => null;
|
||||
|
||||
keyboardManager.addHandler(KeyEventType.KeyDown, [keyCode1], handler1);
|
||||
keyboardManager.addHandler(KeyEventType.KeyDown, [keyCode2], handler2);
|
||||
addHandler(keyboardManager, KeyEventType.KeyDown, [keyCode1], handler1);
|
||||
addHandler(keyboardManager, KeyEventType.KeyDown, [keyCode2], handler2);
|
||||
|
||||
const handlers1 = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCode1);
|
||||
const handlers2 = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCode2);
|
||||
const h1 = keyboardManager.getHandler(KeyEventType.KeyDown, keyCode1);
|
||||
const h2 = keyboardManager.getHandler(KeyEventType.KeyDown, keyCode2);
|
||||
|
||||
expect(handlers1.length).toEqual(1);
|
||||
expect(handlers2.length).toEqual(1);
|
||||
|
||||
expect(handlers1[0]).toBe(handler1);
|
||||
expect(handlers2[0]).toBe(handler2);
|
||||
expect(h1).toBe(handler1);
|
||||
expect(h2).toBe(handler2);
|
||||
});
|
||||
|
||||
it("can register handlers for same key code and different key event types", () => {
|
||||
const keyCodeString = "Ctrl+H";
|
||||
const keyCodes = [keyCodeString];
|
||||
const handler1 = (evt: KeyboardEvent) => null;
|
||||
const handler2 = (evt: KeyboardEvent) => null;
|
||||
const handler3 = (evt: KeyboardEvent) => null;
|
||||
const keyDownHandler = (evt: KeyboardEvent) => null;
|
||||
const keyUpHandler = (evt: KeyboardEvent) => null;
|
||||
const keyPressHandler = (evt: KeyboardEvent) => null;
|
||||
|
||||
keyboardManager.addHandler(KeyEventType.KeyDown, keyCodes, handler1);
|
||||
addHandler(keyboardManager, KeyEventType.KeyDown, keyCodes, keyDownHandler);
|
||||
|
||||
keyboardManager.addHandler(KeyEventType.KeyUp, keyCodes, handler1);
|
||||
keyboardManager.addHandler(KeyEventType.KeyUp, keyCodes, handler2);
|
||||
addHandler(keyboardManager, KeyEventType.KeyUp, keyCodes, keyUpHandler);
|
||||
|
||||
keyboardManager.addHandler(KeyEventType.KeyPress, keyCodes, handler1);
|
||||
keyboardManager.addHandler(KeyEventType.KeyPress, keyCodes, handler2);
|
||||
keyboardManager.addHandler(KeyEventType.KeyPress, keyCodes, handler3);
|
||||
addHandler(keyboardManager, KeyEventType.KeyPress, keyCodes, keyPressHandler);
|
||||
|
||||
const keyDownHandlers = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCodeString);
|
||||
expect(keyDownHandlers.length).toEqual(1);
|
||||
|
||||
const keyUpHandlers = keyboardManager.getHandlers(KeyEventType.KeyUp, keyCodeString);
|
||||
expect(keyUpHandlers.length).toEqual(2);
|
||||
|
||||
const keyPressHandlers = keyboardManager.getHandlers(KeyEventType.KeyPress, keyCodeString);
|
||||
expect(keyPressHandlers.length).toEqual(3);
|
||||
expect(keyboardManager.getHandler(KeyEventType.KeyDown, keyCodeString)).toBe(keyDownHandler);
|
||||
expect(keyboardManager.getHandler(KeyEventType.KeyUp, keyCodeString)).toBe(keyUpHandler);
|
||||
expect(keyboardManager.getHandler(KeyEventType.KeyPress, keyCodeString)).toBe(keyPressHandler);
|
||||
});
|
||||
|
||||
it("can register multiple handlers for same key code", () => {
|
||||
it("throws error when trying to register multiple handlers for same key code", () => {
|
||||
const keyCode = "Ctrl+H";
|
||||
const handler1 = (evt: KeyboardEvent) => null;
|
||||
const handler2 = (evt: KeyboardEvent) => null;
|
||||
|
||||
keyboardManager.addHandler(KeyEventType.KeyDown, [keyCode], handler1);
|
||||
keyboardManager.addHandler(KeyEventType.KeyDown, [keyCode], handler2);
|
||||
|
||||
const handlers = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCode);
|
||||
expect(handlers.length).toEqual(2);
|
||||
});
|
||||
|
||||
it("list of handlers cannot be mutated outside of API", () => {
|
||||
const keyCode = "Ctrl+K";
|
||||
const handler = (evt: KeyboardEvent) => null;
|
||||
|
||||
keyboardManager.addHandler(KeyEventType.KeyDown, [keyCode], handler);
|
||||
const handlers = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCode);
|
||||
const handlerCount = handlers.length;
|
||||
|
||||
// Attempt to add more handlers
|
||||
handlers.push(handler, handler, handler);
|
||||
|
||||
const newHandlers = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCode);
|
||||
expect(newHandlers.length).toEqual(handlerCount);
|
||||
addHandler(keyboardManager, KeyEventType.KeyDown, [keyCode], handler1);
|
||||
expect(() => addHandler(keyboardManager, KeyEventType.KeyDown, [keyCode], handler2)).toThrowError();
|
||||
});
|
||||
|
||||
it("can remove keyboard event handlers", () => {
|
||||
const keyCode = "Ctrl+1";
|
||||
const handler = (evt: KeyboardEvent) => null;
|
||||
|
||||
// Register keyboard handler
|
||||
const deregister = keyboardManager.addHandler(KeyEventType.KeyDown, [keyCode], handler);
|
||||
const deregister = addHandler(keyboardManager, KeyEventType.KeyDown, [keyCode], jest.fn());
|
||||
|
||||
// Get registered handlers
|
||||
let handlers = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCode);
|
||||
expect(handlers.length).toEqual(1);
|
||||
let handler = keyboardManager.getHandler(KeyEventType.KeyDown, keyCode);
|
||||
expect(handler).not.toBeNull();
|
||||
|
||||
// Invoke deregister functions
|
||||
deregister();
|
||||
|
||||
// Get registered handlers after deregistered
|
||||
handlers = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCode);
|
||||
expect(handlers.length).toEqual(0);
|
||||
handler = keyboardManager.getHandler(KeyEventType.KeyDown, keyCode);
|
||||
expect(handler).toBeNull();
|
||||
});
|
||||
|
||||
it("get handlers for unregistered key code returns empty array", () => {
|
||||
const handlers = keyboardManager.getHandlers(KeyEventType.KeyDown, "Alt+1");
|
||||
expect(handlers.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("invokes registered keyboard handlers", () => {
|
||||
const keyCode = "Ctrl+1";
|
||||
const handler1 = jest.fn();
|
||||
const handler2 = jest.fn();
|
||||
|
||||
keyboardManager.addHandler(KeyEventType.KeyDown, [keyCode], handler1);
|
||||
keyboardManager.addHandler(KeyEventType.KeyDown, [keyCode], handler2);
|
||||
|
||||
const keyboardEvent = new KeyboardEvent("keydown", {
|
||||
ctrlKey: true,
|
||||
code: "1",
|
||||
});
|
||||
|
||||
keyboardManager.invokeHandlers(KeyEventType.KeyDown, keyCode, keyboardEvent);
|
||||
|
||||
expect(handler1).toBeCalledWith(keyboardEvent);
|
||||
expect(handler2).toBeCalledWith(keyboardEvent);
|
||||
it("get handler for unregistered key code returns null", () => {
|
||||
const handler = keyboardManager.getHandler(KeyEventType.KeyDown, "Alt+1");
|
||||
expect(handler).toBeNull();
|
||||
});
|
||||
|
||||
describe("array with mulitple keyCodes", () => {
|
||||
@@ -135,15 +98,10 @@ describe("Keyboard Registration Manager", () => {
|
||||
const keyCodes = [keyCode1, keyCode2];
|
||||
const handler = jest.fn();
|
||||
|
||||
keyboardManager.addHandler(KeyEventType.KeyDown, keyCodes, handler);
|
||||
addHandler(keyboardManager, KeyEventType.KeyDown, keyCodes, handler);
|
||||
|
||||
const handlers1 = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCode1);
|
||||
expect(handlers1.length).toEqual(1);
|
||||
expect(handlers1[0]).toEqual(handler);
|
||||
|
||||
const handlers2 = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCode2);
|
||||
expect(handlers2.length).toEqual(1);
|
||||
expect(handlers2[0]).toEqual(handler);
|
||||
expect(keyboardManager.getHandler(KeyEventType.KeyDown, keyCode1)).toBe(handler);
|
||||
expect(keyboardManager.getHandler(KeyEventType.KeyDown, keyCode2)).toBe(handler);
|
||||
});
|
||||
|
||||
it("invoke the registered keyboard handlers", () => {
|
||||
@@ -152,21 +110,21 @@ describe("Keyboard Registration Manager", () => {
|
||||
const keyCodes = [keyCode1, keyCode2];
|
||||
const handler = jest.fn();
|
||||
|
||||
keyboardManager.addHandler(KeyEventType.KeyDown, keyCodes, handler);
|
||||
addHandler(keyboardManager, KeyEventType.KeyDown, keyCodes, handler);
|
||||
|
||||
const keyboardEvent1 = new KeyboardEvent("keydown", {
|
||||
ctrlKey: true,
|
||||
code: "1",
|
||||
});
|
||||
|
||||
keyboardManager.invokeHandlers(KeyEventType.KeyDown, keyCode1, keyboardEvent1);
|
||||
keyboardManager.invokeHandler(KeyEventType.KeyDown, keyCode1, keyboardEvent1);
|
||||
expect(handler).toBeCalledWith(keyboardEvent1);
|
||||
|
||||
const keyboardEvent2 = new KeyboardEvent("keydown", {
|
||||
code: "ArrowUp",
|
||||
});
|
||||
|
||||
keyboardManager.invokeHandlers(KeyEventType.KeyDown, keyCode1, keyboardEvent2);
|
||||
keyboardManager.invokeHandler(KeyEventType.KeyDown, keyCode1, keyboardEvent2);
|
||||
expect(handler).toBeCalledWith(keyboardEvent2);
|
||||
});
|
||||
|
||||
@@ -177,24 +135,24 @@ describe("Keyboard Registration Manager", () => {
|
||||
const handler = (evt: KeyboardEvent) => null;
|
||||
|
||||
// Register keyboard handler
|
||||
const deregister = keyboardManager.addHandler(KeyEventType.KeyDown, keyCodes, handler);
|
||||
const deregister = addHandler(keyboardManager, KeyEventType.KeyDown, keyCodes, handler);
|
||||
|
||||
// Get registered handlers
|
||||
let handlers1 = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCode1);
|
||||
expect(handlers1.length).toEqual(1);
|
||||
let h1 = keyboardManager.getHandler(KeyEventType.KeyDown, keyCode1);
|
||||
expect(h1).toBe(handler);
|
||||
|
||||
let handlers2 = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCode2);
|
||||
expect(handlers2.length).toEqual(1);
|
||||
let h2 = keyboardManager.getHandler(KeyEventType.KeyDown, keyCode2);
|
||||
expect(h2).toBe(handler);
|
||||
|
||||
// Invoke deregister function
|
||||
deregister();
|
||||
|
||||
// Get registered handlers after deregistered
|
||||
handlers1 = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCode1);
|
||||
expect(handlers1.length).toEqual(0);
|
||||
h1 = keyboardManager.getHandler(KeyEventType.KeyDown, keyCode1);
|
||||
expect(h1).toBeNull();
|
||||
|
||||
handlers2 = keyboardManager.getHandlers(KeyEventType.KeyDown, keyCode2);
|
||||
expect(handlers2.length).toEqual(0);
|
||||
h2 = keyboardManager.getHandler(KeyEventType.KeyDown, keyCode2);
|
||||
expect(h2).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import Guard from "../../../../common/guard";
|
||||
import { KeyboardManager, KeyEventType } from "./keyboardManager";
|
||||
import { IKeyboardBindingProps } from "../keyboardBinding/keyboardBinding";
|
||||
import { AppError, ErrorCode } from "../../../../models/applicationState";
|
||||
|
||||
/**
|
||||
* A map of keyboard event registrations
|
||||
*/
|
||||
export interface IKeyboardRegistrations {
|
||||
[keyEventType: string]: {
|
||||
[key: string]: KeyboardEventHandler[],
|
||||
[key: string]: IKeyboardBindingProps,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,16 +24,14 @@ export class KeyboardRegistrationManager {
|
||||
private registrations: IKeyboardRegistrations = {};
|
||||
|
||||
/**
|
||||
* Registers a keyboard event handler for the specified key code
|
||||
* @param keyEventType Type of key event (keydown, keyup, keypress)
|
||||
* @param keyCodes a list of key code and key code combinations, ex) Ctrl+1
|
||||
* @param handler The keyboard event handler
|
||||
*
|
||||
* @returns a function for deregistering the handler
|
||||
* Registers a keyboard binding and returns a function to deregister that binding
|
||||
* @param binding Properties for keyboard binding (type of key event, keyCodes, handler, etc.)
|
||||
* @returns a function for deregistering the keyboard binding
|
||||
*/
|
||||
public addHandler(keyEventType: KeyEventType, keyCodes: string[], handler: KeyboardEventHandler): () => void {
|
||||
public registerBinding = (binding: IKeyboardBindingProps) => {
|
||||
const {keyEventType, accelerators, handler, displayName} = binding;
|
||||
Guard.null(keyEventType);
|
||||
Guard.expression(keyCodes, (keyCodes) => keyCodes.length > 0);
|
||||
Guard.expression(accelerators, (keyCodes) => keyCodes.length > 0);
|
||||
Guard.null(handler);
|
||||
|
||||
let eventTypeRegistrations = this.registrations[keyEventType];
|
||||
@@ -40,22 +40,20 @@ export class KeyboardRegistrationManager {
|
||||
this.registrations[keyEventType] = eventTypeRegistrations;
|
||||
}
|
||||
|
||||
keyCodes.forEach((keyCode) => {
|
||||
let keyRegistrations: KeyboardEventHandler[] = this.registrations[keyEventType][keyCode];
|
||||
if (!keyRegistrations) {
|
||||
keyRegistrations = [];
|
||||
this.registrations[keyEventType][keyCode] = keyRegistrations;
|
||||
accelerators.forEach((keyCode) => {
|
||||
const currentBinding = this.registrations[keyEventType][keyCode];
|
||||
if (currentBinding) {
|
||||
let error = `Key code ${keyCode} on key event "${keyEventType}" `;
|
||||
error += `already has binding registered: "${currentBinding.displayName}." `;
|
||||
error += `Cannot register binding "${displayName}" with the same key code and key event type`;
|
||||
throw new AppError(ErrorCode.OverloadedKeyBinding, error);
|
||||
}
|
||||
|
||||
keyRegistrations.push(handler);
|
||||
this.registrations[keyEventType][keyCode] = binding;
|
||||
});
|
||||
|
||||
return () => {
|
||||
keyCodes.forEach((keyCode) => {
|
||||
const keyRegistrations: KeyboardEventHandler[] = this.registrations[keyEventType][keyCode];
|
||||
const index = keyRegistrations.findIndex((h) => h === handler);
|
||||
|
||||
keyRegistrations.splice(index, 1);
|
||||
binding.accelerators.forEach((keyCode) => {
|
||||
delete this.registrations[binding.keyEventType][keyCode];
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -65,12 +63,16 @@ export class KeyboardRegistrationManager {
|
||||
* @param keyEventType Type of key event (keydown, keyup, keypress)
|
||||
* @param keyCode The key code combination, ex) Ctrl+1
|
||||
*/
|
||||
public getHandlers(keyEventType: KeyEventType, keyCode: string) {
|
||||
public getHandler(keyEventType: KeyEventType, keyCode: string): (evt?: KeyboardEvent) => void {
|
||||
Guard.null(keyEventType);
|
||||
Guard.null(keyCode);
|
||||
|
||||
const keyEventTypeRegs = this.registrations[keyEventType];
|
||||
return (keyEventTypeRegs && keyEventTypeRegs[keyCode]) ? [...keyEventTypeRegs[keyCode]] : [];
|
||||
return (keyEventTypeRegs && keyEventTypeRegs[keyCode])
|
||||
?
|
||||
keyEventTypeRegs[keyCode].handler
|
||||
:
|
||||
null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,11 +81,17 @@ export class KeyboardRegistrationManager {
|
||||
* @param keyCode The key code combination, ex) Ctrl+1
|
||||
* @param evt The keyboard event that was raised
|
||||
*/
|
||||
public invokeHandlers(keyEventType: KeyEventType, keyCode: string, evt: KeyboardEvent) {
|
||||
public invokeHandler(keyEventType: KeyEventType, keyCode: string, evt: KeyboardEvent) {
|
||||
Guard.null(keyCode);
|
||||
Guard.null(evt);
|
||||
|
||||
const handlers = this.getHandlers(keyEventType, keyCode);
|
||||
handlers.forEach((handler) => handler(evt));
|
||||
const handler = this.getHandler(keyEventType, keyCode);
|
||||
if (handler !== null) {
|
||||
handler(evt);
|
||||
}
|
||||
}
|
||||
|
||||
public getRegistrations = () => {
|
||||
return this.registrations;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { SyntheticEvent } from "react";
|
||||
import React, { SyntheticEvent, ReactElement } from "react";
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter } from "reactstrap";
|
||||
|
||||
/**
|
||||
@@ -16,11 +16,12 @@ export type MessageFormatHandler = (...params: any[]) => string;
|
||||
*/
|
||||
export interface IMessageBoxProps {
|
||||
title: string;
|
||||
message: string | Element | MessageFormatHandler;
|
||||
message: string | ReactElement<any> | MessageFormatHandler;
|
||||
params?: any[];
|
||||
onButtonSelect?: (button: HTMLButtonElement) => void;
|
||||
onCancel?: () => void;
|
||||
show?: boolean;
|
||||
hideFooter?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,9 +67,9 @@ export default class MessageBox extends React.Component<IMessageBoxProps, IMessa
|
||||
onClosed={this.onClosed}>
|
||||
<ModalHeader toggle={this.toggle}>{this.props.title}</ModalHeader>
|
||||
<ModalBody>{this.getMessage(this.props.message)}</ModalBody>
|
||||
<ModalFooter onClick={this.onFooterClick}>
|
||||
{!this.props.hideFooter && <ModalFooter onClick={this.onFooterClick}>
|
||||
{this.props.children}
|
||||
</ModalFooter>
|
||||
</ModalFooter>}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -100,7 +101,7 @@ export default class MessageBox extends React.Component<IMessageBoxProps, IMessa
|
||||
}
|
||||
}
|
||||
|
||||
private getMessage = (message: string | MessageFormatHandler | Element) => {
|
||||
private getMessage = (message: string | MessageFormatHandler | ReactElement<any>) => {
|
||||
if (typeof message === "function") {
|
||||
return message.apply(this, this.props.params);
|
||||
} else {
|
||||
|
||||
@@ -48,9 +48,10 @@ describe("Custom Video Player Button Component", () => {
|
||||
|
||||
expect(keyboardBinding.exists()).toBe(true);
|
||||
expect(keyboardBinding.props()).toEqual({
|
||||
displayName: defaultProps.tooltip,
|
||||
keyEventType: KeyEventType.KeyDown,
|
||||
accelerators: props.accelerators,
|
||||
onKeyEvent: props.onClick,
|
||||
handler: props.onClick,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -5,10 +5,11 @@ import { KeyEventType } from "../keyboardManager/keyboardManager";
|
||||
|
||||
export interface ICustomVideoPlayerButtonProps {
|
||||
order: number;
|
||||
onClick: () => void;
|
||||
icon?: string;
|
||||
accelerators?: string[];
|
||||
tooltip?: string;
|
||||
player?: Player;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export class CustomVideoPlayerButton extends React.Component<ICustomVideoPlayerButtonProps> {
|
||||
@@ -17,8 +18,9 @@ export class CustomVideoPlayerButton extends React.Component<ICustomVideoPlayerB
|
||||
<Fragment>
|
||||
{this.props.accelerators &&
|
||||
<KeyboardBinding keyEventType={KeyEventType.KeyDown}
|
||||
displayName={this.props.tooltip}
|
||||
accelerators={this.props.accelerators}
|
||||
onKeyEvent={this.props.onClick} />
|
||||
handler={this.props.onClick} />
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -253,7 +253,7 @@ export default class CanvasHelpers {
|
||||
const defaultTargetY = 0;
|
||||
|
||||
if (boundingBox.height > height || boundingBox.width > width) {
|
||||
throw new AppError(ErrorCode.PasteRegionTooBigError, strings.errors.pasteRegionTooBigError.message);
|
||||
throw new AppError(ErrorCode.PasteRegionTooBig, strings.errors.pasteRegionTooBigError.message);
|
||||
}
|
||||
|
||||
if (!CanvasHelpers.boundingBoxWithin(boundingBox, width, height)) {
|
||||
|
||||
@@ -480,22 +480,22 @@ describe("Editor Page Component", () => {
|
||||
expect(removeAllRegionsConfirm).toBeCalled();
|
||||
});
|
||||
|
||||
it("Calls copy regions with hot key", async () => {
|
||||
it("Calls copy regions with hot key", () => {
|
||||
dispatchKeyEvent("Ctrl+c");
|
||||
expect(copyRegions).toBeCalled();
|
||||
});
|
||||
|
||||
it("Calls cut regions with hot key", async () => {
|
||||
it("Calls cut regions with hot key", () => {
|
||||
dispatchKeyEvent("Ctrl+x");
|
||||
expect(cutRegions).toBeCalled();
|
||||
});
|
||||
|
||||
it("Calls paste regions with hot key", async () => {
|
||||
it("Calls paste regions with hot key", () => {
|
||||
dispatchKeyEvent("Ctrl+v");
|
||||
expect(pasteRegions).toBeCalled();
|
||||
});
|
||||
|
||||
it("Calls remove all regions confirmation with hot key", async () => {
|
||||
it("Calls remove all regions confirmation with hot key", () => {
|
||||
dispatchKeyEvent("Ctrl+Delete");
|
||||
expect(removeAllRegionsConfirm).toBeCalled();
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ import CanvasHelpers from "./canvasHelpers";
|
||||
import { tagColors } from "../../../../common/tagColors";
|
||||
import { ToolbarItemName } from "../../../../registerToolbar";
|
||||
import { SelectionMode } from "vott-ct/lib/js/CanvasTools/Interface/ISelectorSettings";
|
||||
import { strings } from "../../../../common/strings";
|
||||
|
||||
/**
|
||||
* Properties for Editor Page
|
||||
@@ -134,10 +135,12 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
|
||||
<div className="editor-page">
|
||||
{[...Array(10).keys()].map((index) => {
|
||||
return (<KeyboardBinding
|
||||
displayName={strings.editorPage.tags.hotKey.help}
|
||||
key={index}
|
||||
keyEventType={KeyEventType.KeyDown}
|
||||
accelerators={[`${index}`]}
|
||||
onKeyEvent={this.handleTagHotKey} />);
|
||||
icon={"fa-tag"}
|
||||
handler={this.handleTagHotKey} />);
|
||||
})}
|
||||
<div className="editor-page-sidebar bg-lighter-1">
|
||||
<EditorSideBar
|
||||
|
||||
@@ -68,7 +68,7 @@ describe("Editor Toolbar", () => {
|
||||
const toolbar = wrapper.find(EditorToolbar) as ReactWrapper<IEditorToolbarProps, IEditorToolbarState>;
|
||||
const select = toolbar.props().items[0];
|
||||
const toolbarRegistry = ToolbarItemFactory.getToolbarItems();
|
||||
expect(select.config).toHaveProperty("accelerators", ["v", "V" ]);
|
||||
expect(select.config).toHaveProperty("accelerators", ["V", "v" ]);
|
||||
expect(select.config).toEqual(toolbarRegistry[0].config);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
@import '../../../assets/sass/theme.scss';
|
||||
|
||||
.keybinding {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.keybinding-icon {
|
||||
vertical-align: bottom;
|
||||
padding: 4px 15px;
|
||||
}
|
||||
|
||||
.help-key.row {
|
||||
color: #ccc;
|
||||
padding: 2px;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: $lighter-2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { mount } from "enzyme";
|
||||
import React from "react";
|
||||
import MockFactory from "../../../common/mockFactory";
|
||||
import { KeyboardManager } from "../common/keyboardManager/keyboardManager";
|
||||
import { IKeyboardRegistrations,
|
||||
KeyboardRegistrationManager } from "../common/keyboardManager/keyboardRegistrationManager";
|
||||
import { HelpMenu, IHelpMenuProps } from "./helpMenu";
|
||||
jest.mock("../common/keyboardManager/keyboardRegistrationManager");
|
||||
|
||||
describe("Help Menu", () => {
|
||||
function createComponent(props?: IHelpMenuProps) {
|
||||
return mount(
|
||||
<KeyboardManager>
|
||||
<HelpMenu {...props}/>
|
||||
</KeyboardManager>,
|
||||
);
|
||||
}
|
||||
const numberRegistrations = 5;
|
||||
const keyboardRegistrations: IKeyboardRegistrations = MockFactory.createKeyboardRegistrations(numberRegistrations);
|
||||
const registrationMock = KeyboardRegistrationManager as jest.Mocked<typeof KeyboardRegistrationManager>;
|
||||
|
||||
registrationMock.prototype.getRegistrations = jest.fn(() => keyboardRegistrations);
|
||||
registrationMock.prototype.registerBinding = jest.fn(() => jest.fn());
|
||||
|
||||
it("Opens when button is clicked", () => {
|
||||
const wrapper = createComponent();
|
||||
expect(wrapper.exists("div.modal-content")).toBe(false);
|
||||
wrapper.find("div.help-menu-button").simulate("click");
|
||||
wrapper.update();
|
||||
expect(wrapper.exists("div.modal-content")).toBe(true);
|
||||
});
|
||||
|
||||
it("Pulls currently registered keyboard bindings upon opening", async () => {
|
||||
const wrapper = createComponent();
|
||||
expect(wrapper.exists("div.help-key.row")).toBe(false);
|
||||
wrapper.find("div.help-menu-button").simulate("click");
|
||||
expect(wrapper.exists("div.modal-content")).toBe(true);
|
||||
expect(registrationMock.prototype.getRegistrations).toBeCalled();
|
||||
await MockFactory.flushUi();
|
||||
expect(wrapper.find("div.help-key.row")).toHaveLength(numberRegistrations);
|
||||
});
|
||||
|
||||
it("Renders keyboard bindings with icon, key binding and display name", async () => {
|
||||
const wrapper = createComponent();
|
||||
wrapper.find("div.help-menu-button").simulate("click");
|
||||
await MockFactory.flushUi();
|
||||
expect(wrapper.exists(`div.col-1.keybinding-icon.fas.test-icon-1`)).toBe(true);
|
||||
expect(wrapper.find("div.col-4.keybinding-accelerator").first().text()).toEqual("A");
|
||||
expect(wrapper.find("div.col-6.keybinding-name").first().text()).toEqual("Binding 1");
|
||||
});
|
||||
|
||||
it("Calls onClose handler when closed", () => {
|
||||
const onClose = jest.fn();
|
||||
const wrapper = createComponent({onClose});
|
||||
wrapper.find("div.help-menu-button").simulate("click");
|
||||
expect(wrapper.exists("div.modal-content")).toBe(true);
|
||||
wrapper.find("button.close").simulate("click");
|
||||
expect(onClose).toBeCalled();
|
||||
expect(wrapper.exists("div.modal-content")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
import React from "react";
|
||||
import MessageBox from "../common/messageBox/messageBox";
|
||||
import { strings } from "../../../common/strings";
|
||||
import { KeyboardContext, IKeyboardContext, KeyEventType } from "../common/keyboardManager/keyboardManager";
|
||||
import { IKeyboardBindingProps, KeyboardBinding } from "../common/keyboardBinding/keyboardBinding";
|
||||
import "./helpMenu.scss";
|
||||
|
||||
export interface IHelpMenuProps {
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export interface IHelpMenuState {
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
export class HelpMenu extends React.Component<IHelpMenuProps, IHelpMenuState> {
|
||||
public static contextType = KeyboardContext;
|
||||
public context!: IKeyboardContext;
|
||||
|
||||
public state = {
|
||||
show: false,
|
||||
};
|
||||
private icon: string = "fa-question-circle";
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className={"help-menu-button"} onClick={() => this.setState({show: true})}>
|
||||
<i className={`fas ${this.icon}`}/>
|
||||
<KeyboardBinding
|
||||
displayName={strings.editorPage.help.title}
|
||||
accelerators={["Ctrl+H", "Ctrl+h"]}
|
||||
handler={() => this.setState({show: !this.state.show})}
|
||||
icon={this.icon}
|
||||
keyEventType={KeyEventType.KeyDown}
|
||||
/>
|
||||
<MessageBox
|
||||
title={strings.titleBar.help}
|
||||
message={this.getHelpBody()}
|
||||
show={this.state.show}
|
||||
onCancel={this.onClose}
|
||||
hideFooter={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private onClose = () => {
|
||||
this.setState({show: false});
|
||||
if (this.props.onClose) {
|
||||
this.props.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
private getHelpBody = () => {
|
||||
|
||||
const registrations = this.context.keyboard.getRegistrations()[KeyEventType.KeyDown];
|
||||
if (!registrations) {
|
||||
return;
|
||||
}
|
||||
|
||||
const groupKeys = this.groupKeys(registrations);
|
||||
|
||||
return (
|
||||
<div className="help-body container">
|
||||
{
|
||||
groupKeys.map((group) => group.length ? this.getRegistrationRow(group, registrations) : null)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private groupKeys = (registrations: {[key: string]: IKeyboardBindingProps}) => {
|
||||
const allKeys = Object.keys(registrations);
|
||||
const caseConsolidatedKeys = this.consolidateKeyCasings(allKeys);
|
||||
|
||||
const groups = [];
|
||||
const alreadyGrouped = new Set();
|
||||
|
||||
for (const key of caseConsolidatedKeys) {
|
||||
const group = [key];
|
||||
if (!alreadyGrouped.has(key)) {
|
||||
alreadyGrouped.add(key);
|
||||
for (const otherKey of caseConsolidatedKeys) {
|
||||
if (!alreadyGrouped.has(otherKey) &&
|
||||
this.bindingEquals(registrations[key], registrations[otherKey])) {
|
||||
group.push(otherKey);
|
||||
alreadyGrouped.add(otherKey);
|
||||
}
|
||||
}
|
||||
groups.push(group);
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
private bindingEquals(binding1: IKeyboardBindingProps, binding2: IKeyboardBindingProps) {
|
||||
return binding1 && binding2
|
||||
&& binding1.displayName === binding2.displayName
|
||||
&& binding1.handler === binding2.handler;
|
||||
}
|
||||
|
||||
private consolidateKeyCasings = (allKeys: string[]): string[] => {
|
||||
const lowerRegistrations = {};
|
||||
for (const key of allKeys) {
|
||||
const lowerKey = key.toLowerCase();
|
||||
if (!lowerRegistrations[lowerKey]) {
|
||||
lowerRegistrations[lowerKey] = key;
|
||||
}
|
||||
}
|
||||
return Object.keys(lowerRegistrations).map((lowerKey) => lowerRegistrations[lowerKey]);
|
||||
}
|
||||
|
||||
private getRegistrationRow = (group: string[], registrations: {[key: string]: IKeyboardBindingProps}) => {
|
||||
const keyRegistration = registrations[group[0]];
|
||||
if (keyRegistration) {
|
||||
return (
|
||||
<div className={"help-key row"}>
|
||||
<div className={`col-1 keybinding-icon ${(keyRegistration.icon)
|
||||
? `fas ${keyRegistration.icon}` : ""}`}/>
|
||||
<div className="col-4 keybinding-accelerator">{this.stringifyGroup(group)}</div>
|
||||
<div className="col-6 keybinding-name">{keyRegistration.displayName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private stringifyGroup(group: string[]): string {
|
||||
return (group.length < 3) ? group.join(", ") : `${group[0]}-${group[group.length - 1]}`;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import React, { Fragment } from "react";
|
||||
import Menu, { MenuItem, SubMenu, Divider } from "rc-menu";
|
||||
import { PlatformType } from "../../../common/hostProcess";
|
||||
import "./titleBar.scss";
|
||||
import { strings } from "../../../common/strings";
|
||||
import { HelpMenu } from "./helpMenu";
|
||||
|
||||
export interface ITitleBarProps extends React.Props<TitleBar> {
|
||||
icon?: string | JSX.Element;
|
||||
@@ -85,20 +87,24 @@ export class TitleBar extends React.Component<ITitleBarProps, ITitleBarState> {
|
||||
{this.props.children}
|
||||
{this.state.platform === PlatformType.Windows &&
|
||||
<ul>
|
||||
<li title="Minimize" className="btn-window-minimize" onClick={this.minimizeWindow}>
|
||||
<li title={strings.titleBar.minimize} className="btn-window-minimize"
|
||||
onClick={this.minimizeWindow}>
|
||||
<i className="far fa-window-minimize" />
|
||||
</li>
|
||||
{!this.state.maximized &&
|
||||
<li title="Maximize" className="btn-window-maximize" onClick={this.maximizeWindow}>
|
||||
<li title={strings.titleBar.maximize} className="btn-window-maximize"
|
||||
onClick={this.maximizeWindow}>
|
||||
<i className="far fa-window-maximize" />
|
||||
</li>
|
||||
}
|
||||
{this.state.maximized &&
|
||||
<li title="Restore" className="btn-window-restore" onClick={this.unmaximizeWindow}>
|
||||
<li title={strings.titleBar.restore} className="btn-window-restore"
|
||||
onClick={this.unmaximizeWindow}>
|
||||
<i className="far fa-window-restore" />
|
||||
</li>
|
||||
}
|
||||
<li title="Close" className="btn-window-close" onClick={this.closeWindow}>
|
||||
<li title={strings.titleBar.close} className="btn-window-close"
|
||||
onClick={this.closeWindow}>
|
||||
<i className="fas fa-times" />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -75,14 +75,16 @@ export abstract class ToolbarItem extends React.Component<IToolbarItemProps> {
|
||||
{
|
||||
accelerators &&
|
||||
<KeyboardBinding
|
||||
displayName={this.props.tooltip}
|
||||
accelerators={accelerators}
|
||||
onKeyEvent={this.onClick}
|
||||
handler={this.onClick}
|
||||
icon={this.props.icon}
|
||||
keyEventType={KeyEventType.KeyDown}
|
||||
/>
|
||||
}
|
||||
<button type="button"
|
||||
className={className.join(" ")}
|
||||
title={this.props.tooltip}
|
||||
title={this.getTitle()}
|
||||
onClick={this.onClick}>
|
||||
<i className={"fas " + this.props.icon} />
|
||||
</button>
|
||||
@@ -92,6 +94,26 @@ export abstract class ToolbarItem extends React.Component<IToolbarItemProps> {
|
||||
|
||||
protected abstract onItemClick();
|
||||
|
||||
private getTitle = () => {
|
||||
return `${this.props.tooltip}${this.getShortcut()}`;
|
||||
}
|
||||
|
||||
private getShortcut = () => {
|
||||
return ` (${this.consolidateKeyCasings(this.props.accelerators).join(", ")})`;
|
||||
}
|
||||
|
||||
private consolidateKeyCasings = (accelerators: string[]): string[] => {
|
||||
const consolidated: string[] = [];
|
||||
if (accelerators) {
|
||||
for (const a of accelerators) {
|
||||
if (!consolidated.find((item) => item.toLowerCase() === a.toLowerCase())) {
|
||||
consolidated.push(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
return consolidated;
|
||||
}
|
||||
|
||||
private onClick = () => {
|
||||
if (this.onItemClick) {
|
||||
this.onItemClick();
|
||||
|
||||
+11
-11
@@ -36,7 +36,7 @@ export default function registerToolbar() {
|
||||
icon: "fa-mouse-pointer",
|
||||
group: ToolbarItemGroup.Canvas,
|
||||
type: ToolbarItemType.State,
|
||||
accelerators: ["v", "V"],
|
||||
accelerators: ["V", "v"],
|
||||
});
|
||||
|
||||
ToolbarItemFactory.register({
|
||||
@@ -45,7 +45,7 @@ export default function registerToolbar() {
|
||||
icon: "fa-vector-square",
|
||||
group: ToolbarItemGroup.Canvas,
|
||||
type: ToolbarItemType.State,
|
||||
accelerators: ["r", "R"],
|
||||
accelerators: ["R", "r"],
|
||||
});
|
||||
|
||||
ToolbarItemFactory.register({
|
||||
@@ -54,7 +54,7 @@ export default function registerToolbar() {
|
||||
icon: "fa-draw-polygon",
|
||||
group: ToolbarItemGroup.Canvas,
|
||||
type: ToolbarItemType.State,
|
||||
accelerators: ["p", "P"],
|
||||
accelerators: ["P", "p"],
|
||||
});
|
||||
|
||||
ToolbarItemFactory.register({
|
||||
@@ -63,7 +63,7 @@ export default function registerToolbar() {
|
||||
icon: "far fa-clone",
|
||||
group: ToolbarItemGroup.Canvas,
|
||||
type: ToolbarItemType.State,
|
||||
accelerators: ["Ctrl+w", "Ctrl+W"],
|
||||
accelerators: ["Ctrl+W", "Ctrl+w"],
|
||||
});
|
||||
|
||||
ToolbarItemFactory.register({
|
||||
@@ -72,7 +72,7 @@ export default function registerToolbar() {
|
||||
icon: "fa-copy",
|
||||
group: ToolbarItemGroup.Regions,
|
||||
type: ToolbarItemType.Action,
|
||||
accelerators: ["Ctrl+c", "Ctrl+C"],
|
||||
accelerators: ["Ctrl+C", "Ctrl+c"],
|
||||
});
|
||||
|
||||
ToolbarItemFactory.register({
|
||||
@@ -81,7 +81,7 @@ export default function registerToolbar() {
|
||||
icon: "fa-cut",
|
||||
group: ToolbarItemGroup.Regions,
|
||||
type: ToolbarItemType.Action,
|
||||
accelerators: ["Ctrl+x", "Ctrl+X"],
|
||||
accelerators: ["Ctrl+X", "Ctrl+x"],
|
||||
});
|
||||
|
||||
ToolbarItemFactory.register({
|
||||
@@ -90,7 +90,7 @@ export default function registerToolbar() {
|
||||
icon: "fa-paste",
|
||||
group: ToolbarItemGroup.Regions,
|
||||
type: ToolbarItemType.Action,
|
||||
accelerators: ["Ctrl+v", "Ctrl+V"],
|
||||
accelerators: ["Ctrl+V", "Ctrl+v"],
|
||||
});
|
||||
|
||||
ToolbarItemFactory.register({
|
||||
@@ -108,7 +108,7 @@ export default function registerToolbar() {
|
||||
icon: "fas fa-arrow-circle-up",
|
||||
group: ToolbarItemGroup.Navigation,
|
||||
type: ToolbarItemType.Action,
|
||||
accelerators: ["ArrowUp", "w", "W"],
|
||||
accelerators: ["ArrowUp", "W", "w"],
|
||||
});
|
||||
|
||||
ToolbarItemFactory.register({
|
||||
@@ -117,7 +117,7 @@ export default function registerToolbar() {
|
||||
icon: "fas fa-arrow-circle-down",
|
||||
group: ToolbarItemGroup.Navigation,
|
||||
type: ToolbarItemType.Action,
|
||||
accelerators: ["ArrowDown", "s", "S"],
|
||||
accelerators: ["ArrowDown", "S", "s"],
|
||||
});
|
||||
|
||||
ToolbarItemFactory.register({
|
||||
@@ -126,7 +126,7 @@ export default function registerToolbar() {
|
||||
icon: "fa-save",
|
||||
group: ToolbarItemGroup.Project,
|
||||
type: ToolbarItemType.Action,
|
||||
accelerators: ["Ctrl+s", "Ctrl+S"],
|
||||
accelerators: ["Ctrl+S", "Ctrl+s"],
|
||||
}, SaveProject);
|
||||
|
||||
ToolbarItemFactory.register({
|
||||
@@ -135,6 +135,6 @@ export default function registerToolbar() {
|
||||
icon: "fa-external-link-square-alt",
|
||||
group: ToolbarItemGroup.Project,
|
||||
type: ToolbarItemType.Action,
|
||||
accelerators: ["Ctrl+e", "Ctrl+E"],
|
||||
accelerators: ["Ctrl+E", "Ctrl+e"],
|
||||
}, ExportProject);
|
||||
}
|
||||
|
||||
Referência em uma Nova Issue
Bloquear um usuário