fix: Keyboard accessible context menus (#1768)

- Makes menus fully accessible and keyboard driven
- Currently adds 2.8% to initial bundle size due to the inclusion of Reakit and its dependency, popperjs.
- Converts all menus to functional components
- Remove old custom menu system
- Various layout and flow improvements around the menus

closes #1766
This commit is contained in:
Tom Moor
2021-01-13 22:00:25 -08:00
committed by GitHub
parent 47369dd968
commit e8b7782f5e
54 changed files with 1788 additions and 1881 deletions

View File

@ -1,68 +1,69 @@
// @flow
import { inject } from "mobx-react";
import { observer } from "mobx-react";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import { withRouter, type RouterHistory } from "react-router-dom";
import UiStore from "stores/UiStore";
import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import { useMenuState } from "reakit/Menu";
import Document from "models/Document";
import Revision from "models/Revision";
import ContextMenu from "components/ContextMenu";
import MenuItem from "components/ContextMenu/MenuItem";
import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
import Separator from "components/ContextMenu/Separator";
import CopyToClipboard from "components/CopyToClipboard";
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
import useStores from "hooks/useStores";
import { documentHistoryUrl } from "utils/routeHelpers";
type Props = {
onOpen?: () => void,
onClose: () => void,
history: RouterHistory,
type Props = {|
document: Document,
revision: Revision,
iconColor?: string,
className?: string,
label: React.Node,
ui: UiStore,
t: TFunction,
};
|};
class RevisionMenu extends React.Component<Props> {
handleRestore = async (ev: SyntheticEvent<>) => {
ev.preventDefault();
await this.props.document.restore({ revisionId: this.props.revision.id });
const { t } = this.props;
this.props.ui.showToast(t("Document restored"), { type: "success" });
this.props.history.push(this.props.document.url);
};
function RevisionMenu({ document, revision, className, iconColor }: Props) {
const { ui } = useStores();
const menu = useMenuState({ modal: true });
const { t } = useTranslation();
const history = useHistory();
handleCopy = () => {
const { t } = this.props;
this.props.ui.showToast(t("Link copied"), { type: "info" });
};
const handleRestore = React.useCallback(
async (ev: SyntheticEvent<>) => {
ev.preventDefault();
await document.restore({ revisionId: revision.id });
ui.showToast(t("Document restored"), { type: "success" });
history.push(document.url);
},
[history, ui, t, document, revision]
);
render() {
const { className, label, onOpen, onClose, t } = this.props;
const url = `${window.location.origin}${documentHistoryUrl(
this.props.document,
this.props.revision.id
)}`;
const handleCopy = React.useCallback(() => {
ui.showToast(t("Link copied"), { type: "info" });
}, [ui, t]);
return (
<DropdownMenu
onOpen={onOpen}
onClose={onClose}
const url = `${window.location.origin}${documentHistoryUrl(
document,
revision.id
)}`;
return (
<>
<OverflowMenuButton
className={className}
label={label}
>
<DropdownMenuItem onClick={this.handleRestore}>
iconColor={iconColor}
{...menu}
/>
<ContextMenu {...menu} aria-label={t("Revision options")}>
<MenuItem {...menu} onClick={handleRestore}>
{t("Restore version")}
</DropdownMenuItem>
<hr />
<CopyToClipboard text={url} onCopy={this.handleCopy}>
<DropdownMenuItem>{t("Copy link")}</DropdownMenuItem>
</MenuItem>
<Separator />
<CopyToClipboard text={url} onCopy={handleCopy}>
<MenuItem {...menu}>{t("Copy link")}</MenuItem>
</CopyToClipboard>
</DropdownMenu>
);
}
</ContextMenu>
</>
);
}
export default withTranslation()<RevisionMenu>(
withRouter(inject("ui")(RevisionMenu))
);
export default observer(RevisionMenu);