[MM-36428][MM-36434][MM-36435] Keyboard navigation and menu updates for new tab/dropdown layout (#1695)

* [MM-36428][MM-36434][MM-36435] Keyboard navigation and menu updates for new tab/dropdown layout

* Shortcuts for Windows/Linux

* Update config.yml

* Fixed up the shortcuts

* Fixed the new server modal popping up where there are GPO teams only
This commit is contained in:
Devin Binnie
2021-08-16 09:17:45 -04:00
committed by GitHub
parent 52becc3467
commit a79e7aeb4c
9 changed files with 139 additions and 19 deletions

View File

@@ -449,7 +449,7 @@ workflows:
branches: branches:
only: only:
- /^release-\d+(\.\d+){1,2}(-rc.*)?/ - /^release-\d+(\.\d+){1,2}(-rc.*)?/
- pull/1691 - pull/1695
- store_artifacts: - store_artifacts:
# for master/PR builds # for master/PR builds

View File

@@ -23,6 +23,7 @@ export const UPDATE_TEAMS = 'update-teams';
export const DARK_MODE_CHANGE = 'dark_mode_change'; export const DARK_MODE_CHANGE = 'dark_mode_change';
export const GET_DARK_MODE = 'get-dark-mode'; export const GET_DARK_MODE = 'get-dark-mode';
export const USER_ACTIVITY_UPDATE = 'user-activity-update'; export const USER_ACTIVITY_UPDATE = 'user-activity-update';
export const UPDATE_SHORTCUT_MENU = 'update-shortcut-menu';
export const LOAD_RETRY = 'load_retry'; export const LOAD_RETRY = 'load_retry';
export const LOAD_SUCCESS = 'load_success'; export const LOAD_SUCCESS = 'load_success';

View File

@@ -7,7 +7,7 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import electron, {BrowserWindow, IpcMainEvent, IpcMainInvokeEvent, Rectangle} from 'electron'; import electron, {BrowserWindow, globalShortcut, IpcMainEvent, IpcMainInvokeEvent, Rectangle} from 'electron';
import isDev from 'electron-is-dev'; import isDev from 'electron-is-dev';
import installExtension, {REACT_DEVELOPER_TOOLS} from 'electron-devtools-installer'; import installExtension, {REACT_DEVELOPER_TOOLS} from 'electron-devtools-installer';
import log from 'electron-log'; import log from 'electron-log';
@@ -39,6 +39,8 @@ import {
SWITCH_TAB, SWITCH_TAB,
SHOW_EDIT_SERVER_MODAL, SHOW_EDIT_SERVER_MODAL,
SHOW_REMOVE_SERVER_MODAL, SHOW_REMOVE_SERVER_MODAL,
UPDATE_SHORTCUT_MENU,
OPEN_TEAMS_DROPDOWN,
} from 'common/communication'; } from 'common/communication';
import Config from 'common/config'; import Config from 'common/config';
import {getDefaultTeamWithTabsFromTeam} from 'common/tabs/TabView'; import {getDefaultTeamWithTabsFromTeam} from 'common/tabs/TabView';
@@ -96,6 +98,7 @@ let appVersion = null;
let config: Config; let config: Config;
let authManager: AuthManager; let authManager: AuthManager;
let certificateManager: CertificateManager; let certificateManager: CertificateManager;
let didCheckForAddServerModal = false;
/** /**
* Main entry point for the application, ensures that everything initializes in the proper order * Main entry point for the application, ensures that everything initializes in the proper order
@@ -233,6 +236,7 @@ function initializeInterCommunicationEventListeners() {
ipcMain.on(NOTIFY_MENTION, handleMentionNotification); ipcMain.on(NOTIFY_MENTION, handleMentionNotification);
ipcMain.handle('get-app-version', handleAppVersion); ipcMain.handle('get-app-version', handleAppVersion);
ipcMain.on('update-menu', handleUpdateMenuEvent); ipcMain.on('update-menu', handleUpdateMenuEvent);
ipcMain.on(UPDATE_SHORTCUT_MENU, handleUpdateShortcutMenuEvent);
ipcMain.on(FOCUS_BROWSERVIEW, WindowManager.focusBrowserView); ipcMain.on(FOCUS_BROWSERVIEW, WindowManager.focusBrowserView);
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
@@ -301,6 +305,14 @@ function handleConfigSynchronize() {
WindowManager.sendToRenderer(RELOAD_CONFIGURATION); WindowManager.sendToRenderer(RELOAD_CONFIGURATION);
} }
if (process.platform === 'win32' && !didCheckForAddServerModal && typeof config.registryConfigData !== 'undefined') {
didCheckForAddServerModal = true;
if (config.teams.length === 0) {
handleNewServerModal();
}
}
ipcMain.emit('update-menu', true, config);
ipcMain.emit(EMIT_CONFIGURATION, true, config.data); ipcMain.emit(EMIT_CONFIGURATION, true, config.data);
} }
@@ -646,8 +658,11 @@ function initializeAfterAppReady() {
WindowManager.showMainWindow(deeplinkingURL); WindowManager.showMainWindow(deeplinkingURL);
if (config.teams.length === 0) { // only check for non-Windows, as with Windows we have to wait for GPO teams
WindowManager.showSettingsWindow(); if (process.platform !== 'win32' || typeof config.registryConfigData !== 'undefined') {
if (config.teams.length === 0) {
handleNewServerModal();
}
} }
criticalErrorHandler.setMainWindow(WindowManager.getMainWindow()!); criticalErrorHandler.setMainWindow(WindowManager.getMainWindow()!);
@@ -722,6 +737,10 @@ function initializeAfterAppReady() {
// is the requesting url trusted? // is the requesting url trusted?
callback(urlUtils.isTrustedURL(requestingURL, config.teams)); callback(urlUtils.isTrustedURL(requestingURL, config.teams));
}); });
globalShortcut.register(`${process.platform === 'darwin' ? 'Cmd+Ctrl' : 'Ctrl+Shift'}+S`, () => {
ipcMain.emit(OPEN_TEAMS_DROPDOWN);
});
} }
// //
@@ -761,6 +780,10 @@ function handleUpdateMenuEvent(event: IpcMainEvent, menuConfig: Config) {
} }
} }
function handleUpdateShortcutMenuEvent(event: IpcMainEvent) {
handleUpdateMenuEvent(event, config);
}
async function handleSelectDownload(event: IpcMainInvokeEvent, startFrom: string) { async function handleSelectDownload(event: IpcMainInvokeEvent, startFrom: string) {
const message = 'Specify the folder where files will download'; const message = 'Specify the folder where files will download';
const result = await dialog.showOpenDialog({defaultPath: startFrom || config.downloadLocation, const result = await dialog.showOpenDialog({defaultPath: startFrom || config.downloadLocation,

View File

@@ -3,9 +3,9 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
'use strict'; 'use strict';
import {app, Menu, MenuItemConstructorOptions, MenuItem, session, shell, WebContents, webContents} from 'electron'; import {app, ipcMain, Menu, MenuItemConstructorOptions, MenuItem, session, shell, WebContents, webContents} from 'electron';
import {ADD_SERVER} from 'common/communication'; import {SHOW_NEW_SERVER_MODAL} from 'common/communication';
import Config from 'common/config'; import Config from 'common/config';
import * as WindowManager from '../windows/windowManager'; import * as WindowManager from '../windows/windowManager';
@@ -44,7 +44,7 @@ function createTemplate(config: Config) {
platformAppMenu.push({ platformAppMenu.push({
label: 'Sign in to Another Server', label: 'Sign in to Another Server',
click() { click() {
WindowManager.sendToRenderer(ADD_SERVER); ipcMain.emit(SHOW_NEW_SERVER_MODAL);
}, },
}); });
} }
@@ -151,7 +151,7 @@ function createTemplate(config: Config) {
} }
}, },
}, { }, {
label: 'Developer Tools for Current Server', label: 'Developer Tools for Current Tab',
click() { click() {
WindowManager.openBrowserViewDevTools(); WindowManager.openBrowserViewDevTools();
}, },
@@ -206,22 +206,35 @@ function createTemplate(config: Config) {
role: 'close', role: 'close',
accelerator: 'CmdOrCtrl+W', accelerator: 'CmdOrCtrl+W',
}, separatorItem, ...teams.slice(0, 9).sort((teamA, teamB) => teamA.order - teamB.order).map((team, i) => { }, separatorItem, ...teams.slice(0, 9).sort((teamA, teamB) => teamA.order - teamB.order).map((team, i) => {
return { const items = [];
items.push({
label: team.name, label: team.name,
accelerator: `CmdOrCtrl+${i + 1}`, accelerator: `${process.platform === 'darwin' ? 'Cmd+Ctrl' : 'Ctrl+Shift'}+${i + 1}`,
click() { click() {
WindowManager.switchServer(team.name); WindowManager.switchServer(team.name);
}, },
}; });
}), separatorItem, { if (WindowManager.getCurrentTeamName() === team.name) {
label: 'Select Next Server', team.tabs.slice(0, 9).sort((teamA, teamB) => teamA.order - teamB.order).forEach((tab, i) => {
items.push({
label: ` ${tab.name}`, // TODO
accelerator: `CmdOrCtrl+${i + 1}`,
click() {
WindowManager.switchTab(team.name, tab.name);
},
});
});
}
return items;
}).flat(), separatorItem, {
label: 'Select Next Tab',
accelerator: 'Ctrl+Tab', accelerator: 'Ctrl+Tab',
click() { click() {
WindowManager.selectNextTab(); WindowManager.selectNextTab();
}, },
enabled: (teams.length > 1), enabled: (teams.length > 1),
}, { }, {
label: 'Select Previous Server', label: 'Select Previous Tab',
accelerator: 'Ctrl+Shift+Tab', accelerator: 'Ctrl+Shift+Tab',
click() { click() {
WindowManager.selectPreviousTab(); WindowManager.selectPreviousTab();

View File

@@ -7,7 +7,16 @@ import log from 'electron-log';
import {CombinedConfig} from 'types/config'; import {CombinedConfig} from 'types/config';
import {MAXIMIZE_CHANGE, HISTORY, GET_LOADING_SCREEN_DATA, REACT_APP_INITIALIZED, LOADING_SCREEN_ANIMATION_FINISHED, FOCUS_THREE_DOT_MENU, GET_DARK_MODE} from 'common/communication'; import {
MAXIMIZE_CHANGE,
HISTORY,
GET_LOADING_SCREEN_DATA,
REACT_APP_INITIALIZED,
LOADING_SCREEN_ANIMATION_FINISHED,
FOCUS_THREE_DOT_MENU,
GET_DARK_MODE,
UPDATE_SHORTCUT_MENU,
} from 'common/communication';
import urlUtils from 'common/utils/url'; import urlUtils from 'common/utils/url';
import {getTabViewName} from 'common/tabs/TabView'; import {getTabViewName} from 'common/tabs/TabView';
@@ -30,6 +39,7 @@ type WindowManagerStatus = {
config?: CombinedConfig; config?: CombinedConfig;
viewManager?: ViewManager; viewManager?: ViewManager;
teamDropdown?: TeamDropdownView; teamDropdown?: TeamDropdownView;
currentServerName?: string;
}; };
const status: WindowManagerStatus = {}; const status: WindowManagerStatus = {};
@@ -338,6 +348,7 @@ function initializeViewManager() {
status.viewManager = new ViewManager(status.config, status.mainWindow); status.viewManager = new ViewManager(status.config, status.mainWindow);
status.viewManager.load(); status.viewManager.load();
status.viewManager.showInitial(); status.viewManager.showInitial();
status.currentServerName = status.config.teams.find((team) => team.order === 0)?.name;
} }
} }
@@ -348,9 +359,11 @@ export function switchServer(serverName: string) {
log.error('Cannot find server in config'); log.error('Cannot find server in config');
return; return;
} }
status.currentServerName = serverName;
const lastActiveTab = server.tabs[server.lastActiveTab || 0]; const lastActiveTab = server.tabs[server.lastActiveTab || 0];
const tabViewName = getTabViewName(serverName, lastActiveTab.name); const tabViewName = getTabViewName(serverName, lastActiveTab.name);
status.viewManager?.showByName(tabViewName); status.viewManager?.showByName(tabViewName);
ipcMain.emit(UPDATE_SHORTCUT_MENU);
} }
export function switchTab(serverName: string, tabName: string) { export function switchTab(serverName: string, tabName: string) {
@@ -503,3 +516,7 @@ export function selectPreviousTab() {
function handleGetDarkMode() { function handleGetDarkMode() {
return status.config?.darkMode; return status.config?.darkMode;
} }
export function getCurrentTeamName() {
return status.currentServerName;
}

View File

@@ -400,6 +400,7 @@ export default class MainPage extends React.PureComponent<Props, State> {
<DotsVerticalIcon/> <DotsVerticalIcon/>
</button> </button>
<TeamDropdownButton <TeamDropdownButton
isDisabled={this.state.modalOpen}
activeServerName={this.state.activeServerName} activeServerName={this.state.activeServerName}
totalMentionCount={totalMentionCount} totalMentionCount={totalMentionCount}
hasUnreads={totalUnreadCount > 0} hasUnreads={totalUnreadCount > 0}

View File

@@ -10,6 +10,7 @@ import '../css/components/TeamDropdownButton.scss';
import '../css/compass-icons.css'; import '../css/compass-icons.css';
type Props = { type Props = {
isDisabled?: boolean;
activeServerName: string; activeServerName: string;
totalMentionCount: number; totalMentionCount: number;
hasUnreads: boolean; hasUnreads: boolean;
@@ -18,7 +19,7 @@ type Props = {
} }
const TeamDropdownButton: React.FC<Props> = (props: Props) => { const TeamDropdownButton: React.FC<Props> = (props: Props) => {
const {activeServerName, totalMentionCount, hasUnreads, isMenuOpen, darkMode} = props; const {isDisabled, activeServerName, totalMentionCount, hasUnreads, isMenuOpen, darkMode} = props;
const handleToggleButton = (event: React.MouseEvent<HTMLButtonElement>) => { const handleToggleButton = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault(); event.preventDefault();
@@ -41,7 +42,9 @@ const TeamDropdownButton: React.FC<Props> = (props: Props) => {
return ( return (
<button <button
disabled={isDisabled}
className={classNames('TeamDropdownButton', { className={classNames('TeamDropdownButton', {
disabled: isDisabled,
isMenuOpen, isMenuOpen,
darkMode, darkMode,
})} })}

View File

@@ -7,7 +7,11 @@
align-items: center; align-items: center;
font-family: Open Sans; font-family: Open Sans;
&:hover { &.disabled {
opacity: 0.5;
}
&:not(.disabled):hover {
background-color: #f4f4f4; background-color: #f4f4f4;
.TeamDropdownButton__badge-count, .TeamDropdownButton__badge-unreads { .TeamDropdownButton__badge-count, .TeamDropdownButton__badge-unreads {
@@ -15,7 +19,7 @@
} }
} }
&:focus, &.isMenuOpen { &:not(.disabled):focus, &.isMenuOpen {
background-color: #fff; background-color: #fff;
.TeamDropdownButton__badge-count, .TeamDropdownButton__badge-unreads { .TeamDropdownButton__badge-count, .TeamDropdownButton__badge-unreads {

View File

@@ -48,12 +48,19 @@ function getStyle(style?: DraggingStyle | NotDraggingStyle) {
return style; return style;
} }
class TeamDropdown extends React.PureComponent<Record<string, never>, State> { class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
buttonRefs: Map<number, HTMLButtonElement>;
addServerRef: React.RefObject<HTMLButtonElement>;
focusedIndex: number | null;
constructor(props: Record<string, never>) { constructor(props: Record<string, never>) {
super(props); super(props);
this.state = { this.state = {
isAnyDragging: false, isAnyDragging: false,
}; };
this.focusedIndex = null;
this.buttonRefs = new Map();
this.addServerRef = React.createRef();
window.addEventListener('message', this.handleMessageEvent); window.addEventListener('message', this.handleMessageEvent);
} }
@@ -137,6 +144,7 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
componentDidMount() { componentDidMount() {
window.postMessage({type: REQUEST_TEAMS_DROPDOWN_INFO}, window.location.href); window.postMessage({type: REQUEST_TEAMS_DROPDOWN_INFO}, window.location.href);
window.addEventListener('click', this.closeMenu); window.addEventListener('click', this.closeMenu);
window.addEventListener('keydown', this.handleKeyboardShortcuts);
} }
componentDidUpdate() { componentDidUpdate() {
@@ -145,6 +153,53 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
componentWillUnmount() { componentWillUnmount() {
window.removeEventListener('click', this.closeMenu); window.removeEventListener('click', this.closeMenu);
window.removeEventListener('keydown', this.handleKeyboardShortcuts);
}
setButtonRef = (teamIndex: number, refMethod?: (element: HTMLButtonElement) => any) => {
return (ref: HTMLButtonElement) => {
this.addButtonRef(teamIndex, ref);
refMethod?.(ref);
};
}
addButtonRef = (teamIndex: number, ref: HTMLButtonElement | null) => {
if (ref) {
this.buttonRefs.set(teamIndex, ref);
ref.addEventListener('focusin', () => {
this.focusedIndex = teamIndex;
});
ref.addEventListener('blur', () => {
this.focusedIndex = null;
});
}
}
handleKeyboardShortcuts = (event: KeyboardEvent) => {
if (event.key === 'ArrowDown') {
if (this.focusedIndex === null) {
this.focusedIndex = 0;
} else {
this.focusedIndex = (this.focusedIndex + 1) % this.buttonRefs.size;
}
this.buttonRefs.get(this.focusedIndex)?.focus();
}
if (event.key === 'ArrowUp') {
if (this.focusedIndex === null || this.focusedIndex === 0) {
this.focusedIndex = this.buttonRefs.size - 1;
} else {
this.focusedIndex = (this.focusedIndex - 1) % this.buttonRefs.size;
}
this.buttonRefs.get(this.focusedIndex)?.focus();
}
if (event.key === 'Escape') {
this.closeMenu();
}
this.buttonRefs.forEach((button, index) => {
if (event.key === String(index + 1)) {
button.focus();
}
});
} }
handleClickOnDragHandle = (event: React.MouseEvent<HTMLDivElement>) => { handleClickOnDragHandle = (event: React.MouseEvent<HTMLDivElement>) => {
@@ -238,7 +293,7 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
anyDragging: this.state.isAnyDragging, anyDragging: this.state.isAnyDragging,
active: this.isActiveTeam(team), active: this.isActiveTeam(team),
})} })}
ref={provided.innerRef} ref={this.setButtonRef(orderedIndex, provided.innerRef)}
{...provided.draggableProps} {...provided.draggableProps}
onClick={this.selectServer(team)} onClick={this.selectServer(team)}
style={getStyle(provided.draggableProps.style)} style={getStyle(provided.draggableProps.style)}
@@ -284,6 +339,9 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
<hr className='TeamDropdown__divider'/> <hr className='TeamDropdown__divider'/>
{this.state.enableServerManagement && {this.state.enableServerManagement &&
<button <button
ref={(ref) => {
this.addButtonRef(this.state.orderedTeams?.length || 0, ref);
}}
className='TeamDropdown__button addServer' className='TeamDropdown__button addServer'
onClick={this.addServer} onClick={this.addServer}
> >