feat: add account switcher menu (#941)

This commit is contained in:
Aman Harwara
2022-03-22 21:59:06 +05:30
committed by GitHub
parent 57469d6b2d
commit a764987b20
11 changed files with 273 additions and 225 deletions

View File

@@ -0,0 +1,82 @@
import { Icon } from '@/components/Icon';
import { MenuItem, MenuItemType } from '@/components/Menu/MenuItem';
import { KeyboardKey } from '@/services/ioService';
import { ApplicationDescriptor } from '@standardnotes/snjs/dist/@types';
import { FunctionComponent } from 'preact';
import { useEffect, useRef, useState } from 'preact/hooks';
type Props = {
descriptor: ApplicationDescriptor;
onClick: () => void;
onDelete: () => void;
renameDescriptor: (label: string) => void;
};
export const WorkspaceMenuItem: FunctionComponent<Props> = ({
descriptor,
onClick,
onDelete,
renameDescriptor,
}) => {
const [isRenaming, setIsRenaming] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (isRenaming) {
inputRef.current?.focus();
}
}, [isRenaming]);
const handleInputKeyDown = (event: KeyboardEvent) => {
if (event.key === KeyboardKey.Enter) {
inputRef.current?.blur();
}
};
const handleInputBlur = (event: FocusEvent) => {
const name = (event.target as HTMLInputElement).value;
renameDescriptor(name);
setIsRenaming(false);
};
return (
<MenuItem
type={MenuItemType.RadioButton}
className="sn-dropdown-item py-2 focus:bg-info-backdrop focus:shadow-none"
onClick={onClick}
checked={descriptor.primary}
>
<div className="flex items-center justify-between w-full">
{isRenaming ? (
<input
ref={inputRef}
type="text"
value={descriptor.label}
onKeyDown={handleInputKeyDown}
onBlur={handleInputBlur}
/>
) : (
<div>{descriptor.label}</div>
)}
{descriptor.primary && (
<div>
<button
className="w-5 h-5 p-0 mr-3 border-0 bg-transparent hover:bg-contrast cursor-pointer"
onClick={() => {
setIsRenaming((isRenaming) => !isRenaming);
}}
>
<Icon type="pencil" className="sn-icon--mid color-neutral" />
</button>
<button
className="w-5 h-5 p-0 border-0 bg-transparent hover:bg-contrast cursor-pointer"
onClick={onDelete}
>
<Icon type="trash" className="sn-icon--mid color-danger" />
</button>
</div>
)}
</div>
</MenuItem>
);
};

View File

@@ -0,0 +1,69 @@
import { ApplicationGroup } from '@/ui_models/application_group';
import { AppState } from '@/ui_models/app_state';
import { ApplicationDescriptor } from '@standardnotes/snjs';
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
import { useEffect, useState } from 'preact/hooks';
import { Icon } from '../../Icon';
import { Menu } from '../../Menu/Menu';
import { MenuItem, MenuItemSeparator, MenuItemType } from '../../Menu/MenuItem';
import { WorkspaceMenuItem } from './WorkspaceMenuItem';
type Props = {
mainApplicationGroup: ApplicationGroup;
appState: AppState;
isOpen: boolean;
};
export const WorkspaceSwitcherMenu: FunctionComponent<Props> = observer(
({ mainApplicationGroup, appState, isOpen }) => {
const [applicationDescriptors, setApplicationDescriptors] = useState<
ApplicationDescriptor[]
>([]);
useEffect(() => {
const removeAppGroupObserver =
mainApplicationGroup.addApplicationChangeObserver(() => {
const applicationDescriptors = mainApplicationGroup.getDescriptors();
setApplicationDescriptors(applicationDescriptors);
});
return () => {
removeAppGroupObserver();
};
}, [mainApplicationGroup]);
return (
<Menu
a11yLabel="Workspace switcher menu"
className="px-0 focus:shadow-none"
isOpen={isOpen}
>
{applicationDescriptors.map((descriptor) => (
<WorkspaceMenuItem
descriptor={descriptor}
onDelete={() => {
appState.accountMenu.setSigningOut(true);
}}
onClick={() => {
mainApplicationGroup.loadApplicationForDescriptor(descriptor);
}}
renameDescriptor={(label: string) =>
mainApplicationGroup.renameDescriptor(descriptor, label)
}
/>
))}
<MenuItemSeparator />
<MenuItem
type={MenuItemType.IconButton}
onClick={() => {
mainApplicationGroup.addNewApplication();
}}
>
<Icon type="user-add" className="color-neutral mr-2" />
Add another workspace
</MenuItem>
</Menu>
);
}
);

View File

@@ -0,0 +1,83 @@
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/constants';
import { ApplicationGroup } from '@/ui_models/application_group';
import { AppState } from '@/ui_models/app_state';
import {
calculateSubmenuStyle,
SubmenuStyle,
} from '@/utils/calculateSubmenuStyle';
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
import { useEffect, useRef, useState } from 'preact/hooks';
import { Icon } from '../../Icon';
import { WorkspaceSwitcherMenu } from './WorkspaceSwitcherMenu';
type Props = {
mainApplicationGroup: ApplicationGroup;
appState: AppState;
};
export const WorkspaceSwitcherOption: FunctionComponent<Props> = observer(
({ mainApplicationGroup, appState }) => {
const buttonRef = useRef<HTMLButtonElement>(null);
const menuRef = useRef<HTMLDivElement>(null);
const [isOpen, setIsOpen] = useState(false);
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>();
const toggleMenu = () => {
if (!isOpen) {
const menuPosition = calculateSubmenuStyle(buttonRef.current);
if (menuPosition) {
setMenuStyle(menuPosition);
}
}
setIsOpen(!isOpen);
};
useEffect(() => {
if (isOpen) {
setTimeout(() => {
const newMenuPosition = calculateSubmenuStyle(
buttonRef.current,
menuRef.current
);
if (newMenuPosition) {
setMenuStyle(newMenuPosition);
}
});
}
}, [isOpen]);
return (
<>
<button
ref={buttonRef}
className="sn-dropdown-item justify-between focus:bg-info-backdrop focus:shadow-none"
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
role="menuitem"
onClick={toggleMenu}
>
<div className="flex items-center">
<Icon type="user-switch" className="color-neutral mr-2" />
Switch workspace
</div>
<Icon type="chevron-right" className="color-neutral" />
</button>
{isOpen && (
<div
ref={menuRef}
className="sn-dropdown max-h-120 min-w-68 py-2 fixed overflow-y-auto"
style={menuStyle}
>
<WorkspaceSwitcherMenu
mainApplicationGroup={mainApplicationGroup}
appState={appState}
isOpen={isOpen}
/>
</div>
)}
</>
);
}
);