fix: Fade out navigation when editing and mouse hasn't moved (#3256)

* fix: hide header when editing

* fix: settings collab switch

* Update app/hooks/useMouseMove.ts

Co-authored-by: Tom Moor <tom.moor@gmail.com>

* fix: accept timeout parameter

* fix: don't hide observing banner

* fix: hide on focused and observing

* perf: memo

* hide References too

Co-authored-by: Tom Moor <tom.moor@gmail.com>
This commit is contained in:
Saumya Pandey
2022-05-07 00:17:09 +05:30
committed by GitHub
parent d9e7baf072
commit e0cf873a36
9 changed files with 240 additions and 158 deletions

View File

@@ -83,4 +83,4 @@ const Meta = styled(DocumentMeta)<{ rtl?: boolean }>`
}
`;
export default DocumentMetaWithViews;
export default React.memo(DocumentMetaWithViews);

View File

@@ -21,7 +21,7 @@ const searcher = new FuzzySearch<{
sort: true,
});
class EmojiMenu extends React.Component<
class EmojiMenu extends React.PureComponent<
Omit<
Props<Emoji>,
| "renderMenuItem"

28
app/hooks/useMouseMove.ts Normal file
View File

@@ -0,0 +1,28 @@
import * as React from "react";
/**
* Hook to check if mouse is moving in the window
* @param {number} [timeout] - time in ms to wait before marking the mouse as not moving
* @returns {boolean} true if the mouse is moving, false otherwise
*/
const useMouseMove = (timeout = 5000) => {
const [isMouseMoving, setIsMouseMoving] = React.useState(false);
const timeoutId = React.useRef<ReturnType<typeof setTimeout>>();
const onMouseMove = React.useCallback(() => {
timeoutId.current && clearTimeout(timeoutId.current);
setIsMouseMoving(true);
timeoutId.current = setTimeout(() => setIsMouseMoving(false), timeout);
}, [timeout]);
React.useEffect(() => {
document.addEventListener("mousemove", onMouseMove);
return () => {
document.removeEventListener("mousemove", onMouseMove);
};
}, [onMouseMove]);
return isMouseMoving;
};
export default useMouseMove;

View File

@@ -5,7 +5,7 @@ import parseTitle from "@shared/utils/parseTitle";
import unescape from "@shared/utils/unescape";
import DocumentsStore from "~/stores/DocumentsStore";
import User from "~/models/User";
import { NavigationNode } from "~/types";
import type { NavigationNode } from "~/types";
import Storage from "~/utils/Storage";
import ParanoidModel from "./ParanoidModel";
import View from "./View";

View File

@@ -87,6 +87,9 @@ class DocumentScene extends React.Component<Props> {
@observable
isEditorDirty = false;
@observable
isEditorFocused = false;
@observable
isEmpty = true;
@@ -412,6 +415,9 @@ class DocumentScene extends React.Component<Props> {
}
};
onBlur = () => (this.isEditorFocused = false);
onFocus = () => (this.isEditorFocused = true);
render() {
const {
document,
@@ -449,6 +455,9 @@ class DocumentScene extends React.Component<Props> {
? this.props.match.url
: updateDocumentUrl(this.props.match.url, document);
const isFocusing =
!readOnly || this.isEditorFocused || !!ui.observingUserId;
return (
<ErrorBoundary>
{this.props.location.pathname !== canonicalUrl && (
@@ -538,6 +547,7 @@ class DocumentScene extends React.Component<Props> {
shareId={shareId}
isRevision={!!revision}
isDraft={document.isDraft}
isFocusing={isFocusing}
isEditing={!readOnly && !team?.collaborativeEditing}
isSaving={this.isSaving}
isPublishing={this.isPublishing}
@@ -579,6 +589,8 @@ class DocumentScene extends React.Component<Props> {
document={document}
value={readOnly ? value : undefined}
defaultValue={value}
onBlur={this.onBlur}
onFocus={this.onFocus}
embedsDisabled={embedsDisabled}
onSynced={this.onSynced}
onFileUploadStart={this.onFileUploadStart}
@@ -606,7 +618,10 @@ class DocumentScene extends React.Component<Props> {
<>
<MarkAsViewed document={document} />
<ReferencesWrapper isOnlyTitle={document.isOnlyTitle}>
<References document={document} />
<References
isFocusing={isFocusing}
document={document}
/>
</ReferencesWrapper>
</>
)}

View File

@@ -0,0 +1,11 @@
import * as React from "react";
import styled from "styled-components";
import Flex from "~/components/Flex";
const FadeOut = styled(Flex)<{ $fade: boolean }>`
opacity: ${(props) => (props.$fade ? 0 : 1)};
visibility: ${(props) => (props.$fade ? "hidden" : "visible")};
transition: opacity 900ms ease-in-out, visibility ease-in-out 900ms;
`;
export default React.memo(FadeOut);

View File

@@ -21,6 +21,7 @@ import DocumentBreadcrumb from "~/components/DocumentBreadcrumb";
import Header from "~/components/Header";
import Tooltip from "~/components/Tooltip";
import useMobile from "~/hooks/useMobile";
import useMouseMove from "~/hooks/useMouseMove";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import DocumentMenu from "~/menus/DocumentMenu";
@@ -30,6 +31,7 @@ import TemplatesMenu from "~/menus/TemplatesMenu";
import { NavigationNode } from "~/types";
import { metaDisplay } from "~/utils/keyboard";
import { newDocumentPath, editDocumentUrl } from "~/utils/routeHelpers";
import FadeOut from "./FadeOut";
import ObservingBanner from "./ObservingBanner";
import PublicBreadcrumb from "./PublicBreadcrumb";
import ShareButton from "./ShareButton";
@@ -41,6 +43,7 @@ type Props = {
shareId: string | null | undefined;
isDraft: boolean;
isEditing: boolean;
isFocusing: boolean;
isRevision: boolean;
isSaving: boolean;
isPublishing: boolean;
@@ -67,6 +70,7 @@ function DocumentHeader({
isDraft,
isPublishing,
isRevision,
isFocusing,
isSaving,
savingIsDisabled,
publishingIsDisabled,
@@ -80,6 +84,8 @@ function DocumentHeader({
const { resolvedTheme } = ui;
const { team } = auth;
const isMobile = useMobile();
const isMouseMoving = useMouseMove();
const hideHeader = isFocusing && !isMouseMoving;
// We cache this value for as long as the component is mounted so that if you
// apply a template there is still the option to replace it until the user
@@ -163,6 +169,35 @@ function DocumentHeader({
</Action>
);
const DocumentMenuLabel = React.useCallback(
(props) => (
<Button
icon={<MoreIcon />}
iconColor="currentColor"
{...props}
borderOnHover
neutral
/>
),
[]
);
const NewChildDocLabel = React.useCallback(
(props) => (
<Tooltip
tooltip={t("New document")}
shortcut="n"
delay={500}
placement="bottom"
>
<Button icon={<PlusIcon />} {...props} neutral>
{t("New doc")}
</Button>
</Tooltip>
),
[t]
);
if (shareId) {
return (
<Header
@@ -196,13 +231,15 @@ function DocumentHeader({
<Header
hasSidebar
breadcrumb={
isMobile ? (
<FadeOut $fade={hideHeader}>
{isMobile ? (
<TableOfContentsMenu headings={headings} />
) : (
<DocumentBreadcrumb document={document}>
{!isEditing && toc}
</DocumentBreadcrumb>
)
)}
</FadeOut>
}
title={
<>
@@ -213,12 +250,14 @@ function DocumentHeader({
actions={
<>
<ObservingBanner />
<FadeOut $fade={hideHeader}>
{!isPublishing && isSaving && !team?.collaborativeEditing && (
<Status>{t("Saving")}</Status>
)}
{!isDeleted && <Collaborators document={document} />}
{(isEditing || team?.collaborativeEditing) && !isTemplate && isNew && (
{(isEditing || team?.collaborativeEditing) &&
!isTemplate &&
isNew && (
<Action>
<TemplatesMenu
document={document}
@@ -256,18 +295,7 @@ function DocumentHeader({
<Action>
<NewChildDocumentMenu
document={document}
label={(props) => (
<Tooltip
tooltip={t("New document")}
shortcut="n"
delay={500}
placement="bottom"
>
<Button icon={<PlusIcon />} {...props} neutral>
{t("New doc")}
</Button>
</Tooltip>
)}
label={NewChildDocLabel}
/>
</Action>
)}
@@ -309,21 +337,14 @@ function DocumentHeader({
<DocumentMenu
document={document}
isRevision={isRevision}
label={(props) => (
<Button
icon={<MoreIcon />}
iconColor="currentColor"
{...props}
borderOnHover
neutral
/>
)}
label={DocumentMenuLabel}
showToggleEmbeds={canToggleEmbeds}
showDisplayOptions
/>
</Action>
</>
)}
</FadeOut>
</>
}
/>

View File

@@ -7,16 +7,21 @@ import Document from "~/models/Document";
import Fade from "~/components/Fade";
import Tab from "~/components/Tab";
import Tabs from "~/components/Tabs";
import useMouseMove from "~/hooks/useMouseMove";
import useStores from "~/hooks/useStores";
import FadeOut from "./FadeOut";
import ReferenceListItem from "./ReferenceListItem";
type Props = {
document: Document;
isFocusing: boolean;
};
function References({ document }: Props) {
function References({ document, isFocusing }: Props) {
const { collections, documents } = useStores();
const location = useLocation();
const isMouseMoving = useMouseMove();
const hideHeader = isFocusing && !isMouseMoving;
React.useEffect(() => {
documents.fetchBacklinks(document.id);
@@ -33,7 +38,8 @@ function References({ document }: Props) {
const height = Math.max(backlinks.length, children.length) * 40;
return showBacklinks || showChildDocuments ? (
<Fade>
<FadeOut $fade={hideHeader}>
<Fade style={{ width: "100%" }}>
<Tabs>
{showChildDocuments && (
<Tab to="#children" isActive={() => !isBacklinksTab}>
@@ -79,6 +85,7 @@ function References({ document }: Props) {
)}
</Content>
</Fade>
</FadeOut>
) : null;
}

View File

@@ -445,4 +445,4 @@ const Label = styled.dd`
color: ${(props) => props.theme.textSecondary};
`;
export default KeyboardShortcuts;
export default React.memo(KeyboardShortcuts);