feat: add account switcher menu (#941)
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user