diff --git a/CHANGELOG.md b/CHANGELOG.md index b62469558..5946622d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [1997](https://github.com/microsoft/BotFramework-Emulator/pull/1997) - [1999](https://github.com/microsoft/BotFramework-Emulator/pull/1999) - [2000](https://github.com/microsoft/BotFramework-Emulator/pull/2000) + - [2001](https://github.com/microsoft/BotFramework-Emulator/pull/2001) - [2009](https://github.com/microsoft/BotFramework-Emulator/pull/2009) - [2010](https://github.com/microsoft/BotFramework-Emulator/pull/2010) diff --git a/packages/app/client/src/utils/eventHandlers.spec.ts b/packages/app/client/src/utils/eventHandlers.spec.ts index 782de0bad..61d98d571 100644 --- a/packages/app/client/src/utils/eventHandlers.spec.ts +++ b/packages/app/client/src/utils/eventHandlers.spec.ts @@ -84,6 +84,19 @@ jest.mock('electron', () => ({ }, })); +const mockDOM = ` + +
+ + +
+ +
+
+`; + describe('#globalHandlers', () => { let commandService: CommandServiceImpl; @@ -253,4 +266,30 @@ describe('#globalHandlers', () => { expect(mockRemoteCommandsCalled).toHaveLength(1); expect(mockRemoteCommandsCalled[0].commandName).toBe(ToggleDevTools); }); + + it('should move focus to first element when Tab is pressed', async () => { + const event = new KeyboardEvent('keydown', { key: 'Tab' }); + Object.defineProperty(process, 'platform', { value: 'darwin' }); + + document.body.innerHTML = mockDOM; + var mockFirstElement = document.getElementById('navBtn'); + var mockLastElement = document.getElementById('btn3'); + mockLastElement.focus(); + await globalHandlers(event); + + expect(document.activeElement.id).toBe(mockFirstElement.id); + }); + + it('should move focus to last element when Shift+Tab is pressed', async () => { + const event = new KeyboardEvent('keydown', { shiftKey: true, key: 'Tab' }); + Object.defineProperty(process, 'platform', { value: 'darwin' }); + + document.body.innerHTML = mockDOM; + var mockFirstElement = document.getElementById('navBtn'); + var mockLastElement = document.getElementById('btn3'); + mockFirstElement.focus(); + await globalHandlers(event); + + expect(document.activeElement.id).toBe(mockLastElement.id); + }); }); diff --git a/packages/app/client/src/utils/eventHandlers.ts b/packages/app/client/src/utils/eventHandlers.ts index ec8447058..faaf04f2c 100644 --- a/packages/app/client/src/utils/eventHandlers.ts +++ b/packages/app/client/src/utils/eventHandlers.ts @@ -30,23 +30,45 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // - import { Notification, NotificationType, SharedConstants } from '@bfemulator/app-shared'; import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared'; import { remote } from 'electron'; +import { isMac } from '../../../../app/main/src/utils/platform'; const maxZoomFactor = 3; // 300% const minZoomFactor = 0.25; // 25%; - class EventHandlers { @CommandServiceInstance() public static commandService: CommandServiceImpl; + private static getLastChildWithChildren(node) { + for (let index = node.children.length; index > 0; index--) { + if (node.children[index - 1].children.length > 0 && !node.children[index - 1].hidden) { + return node.children[index - 1]; + } + } + } + + private static getLastDecendants(node, list = []) { + if (node.children.length > 0) { + const child = this.getLastChildWithChildren(node); + if (child) { + list.push(child); + this.getLastDecendants(child, list); + } + } + + const result = [].filter.call(list[list.length - 1].children, element => !element.hasAttribute('disabled')); + + return result; + } + public static async globalHandles(event: KeyboardEvent): Promise { // Meta corresponds to 'Command' on Mac const ctrlOrCmdPressed = event.ctrlKey || event.metaKey; const shiftPressed = event.shiftKey; const key = event.key.toLowerCase(); + const keyCode = event.keyCode; const { Commands: { Electron: { ToggleDevTools }, @@ -54,23 +76,19 @@ class EventHandlers { Notifications: { Add }, }, } = SharedConstants; - let awaitable: Promise; // Ctrl+O if (ctrlOrCmdPressed && key === 'o') { awaitable = EventHandlers.commandService.call(ShowOpenBotDialog); } - // Ctrl+N if (ctrlOrCmdPressed && key === 'n') { awaitable = EventHandlers.commandService.call(ShowBotCreationDialog); } - // Ctrl+0 if (ctrlOrCmdPressed && key === '0') { remote.getCurrentWebContents().setZoomLevel(0); } - // Ctrl+= or Ctrl+Shift+= if (ctrlOrCmdPressed && (key === '=' || key === '+')) { const webContents = remote.getCurrentWebContents(); @@ -83,7 +101,6 @@ class EventHandlers { } }); } - // Ctrl+- or Ctrl+Shift+- if (ctrlOrCmdPressed && (key === '-' || key === '_')) { const webContents = remote.getCurrentWebContents(); @@ -96,18 +113,15 @@ class EventHandlers { } }); } - // F11 if (key === 'f11') { const currentWindow = remote.getCurrentWindow(); currentWindow.setFullScreen(!currentWindow.isFullScreen()); } - // Ctrl+Shift+I if (ctrlOrCmdPressed && shiftPressed && key === 'i') { awaitable = EventHandlers.commandService.remoteCall(ToggleDevTools); } - if (awaitable) { // Prevents the char from showing up if an input is focused event.preventDefault(); @@ -121,7 +135,25 @@ class EventHandlers { } as Notification); } } + + if (isMac()) { + const tabPressed: boolean = key === 'tab'; + const lastDecendants = EventHandlers.getLastDecendants(document.querySelector('main')); + const firstElement = document.querySelector('nav').firstElementChild as HTMLElement; + const lastElement = lastDecendants[lastDecendants.length - 1] as HTMLElement; + const isFirstElement: boolean = document.activeElement === firstElement; + const isLastElement: boolean = document.activeElement === lastElement; + + if (tabPressed) { + if (shiftPressed && isFirstElement) { + lastElement.focus(); + event.preventDefault(); + } else if (!shiftPressed && isLastElement) { + firstElement.focus(); + event.preventDefault(); + } + } + } } } - export const globalHandlers = EventHandlers.globalHandles;