From e0cf873a361d82cc003cd27d473067a989b966ec Mon Sep 17 00:00:00 2001 From: Saumya Pandey Date: Sat, 7 May 2022 00:17:09 +0530 Subject: [PATCH] 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 * 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 --- app/components/DocumentMetaWithViews.tsx | 2 +- app/editor/components/EmojiMenu.tsx | 2 +- app/hooks/useMouseMove.ts | 28 +++ app/models/Document.ts | 2 +- app/scenes/Document/components/Document.tsx | 17 +- app/scenes/Document/components/FadeOut.tsx | 11 + app/scenes/Document/components/Header.tsx | 237 ++++++++++-------- app/scenes/Document/components/References.tsx | 97 +++---- app/scenes/KeyboardShortcuts.tsx | 2 +- 9 files changed, 240 insertions(+), 158 deletions(-) create mode 100644 app/hooks/useMouseMove.ts create mode 100644 app/scenes/Document/components/FadeOut.tsx diff --git a/app/components/DocumentMetaWithViews.tsx b/app/components/DocumentMetaWithViews.tsx index c147b06dd..58de76d78 100644 --- a/app/components/DocumentMetaWithViews.tsx +++ b/app/components/DocumentMetaWithViews.tsx @@ -83,4 +83,4 @@ const Meta = styled(DocumentMeta)<{ rtl?: boolean }>` } `; -export default DocumentMetaWithViews; +export default React.memo(DocumentMetaWithViews); diff --git a/app/editor/components/EmojiMenu.tsx b/app/editor/components/EmojiMenu.tsx index 9e14c561e..28e9be245 100644 --- a/app/editor/components/EmojiMenu.tsx +++ b/app/editor/components/EmojiMenu.tsx @@ -21,7 +21,7 @@ const searcher = new FuzzySearch<{ sort: true, }); -class EmojiMenu extends React.Component< +class EmojiMenu extends React.PureComponent< Omit< Props, | "renderMenuItem" diff --git a/app/hooks/useMouseMove.ts b/app/hooks/useMouseMove.ts new file mode 100644 index 000000000..0b31112ca --- /dev/null +++ b/app/hooks/useMouseMove.ts @@ -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>(); + + 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; diff --git a/app/models/Document.ts b/app/models/Document.ts index dd4bc375a..2d7d153c0 100644 --- a/app/models/Document.ts +++ b/app/models/Document.ts @@ -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"; diff --git a/app/scenes/Document/components/Document.tsx b/app/scenes/Document/components/Document.tsx index 4d8039f15..15c373781 100644 --- a/app/scenes/Document/components/Document.tsx +++ b/app/scenes/Document/components/Document.tsx @@ -87,6 +87,9 @@ class DocumentScene extends React.Component { @observable isEditorDirty = false; + @observable + isEditorFocused = false; + @observable isEmpty = true; @@ -412,6 +415,9 @@ class DocumentScene extends React.Component { } }; + onBlur = () => (this.isEditorFocused = false); + onFocus = () => (this.isEditorFocused = true); + render() { const { document, @@ -449,6 +455,9 @@ class DocumentScene extends React.Component { ? this.props.match.url : updateDocumentUrl(this.props.match.url, document); + const isFocusing = + !readOnly || this.isEditorFocused || !!ui.observingUserId; + return ( {this.props.location.pathname !== canonicalUrl && ( @@ -538,6 +547,7 @@ class DocumentScene extends React.Component { 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 { 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 { <> - + )} diff --git a/app/scenes/Document/components/FadeOut.tsx b/app/scenes/Document/components/FadeOut.tsx new file mode 100644 index 000000000..fa5554b50 --- /dev/null +++ b/app/scenes/Document/components/FadeOut.tsx @@ -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); diff --git a/app/scenes/Document/components/Header.tsx b/app/scenes/Document/components/Header.tsx index d17eeb9c4..81e465bab 100644 --- a/app/scenes/Document/components/Header.tsx +++ b/app/scenes/Document/components/Header.tsx @@ -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({ ); + const DocumentMenuLabel = React.useCallback( + (props) => ( + + + ), + [t] + ); + if (shareId) { return (
- ) : ( - - {!isEditing && toc} - - ) + + {isMobile ? ( + + ) : ( + + {!isEditing && toc} + + )} + } title={ <> @@ -213,117 +250,101 @@ function DocumentHeader({ actions={ <> - - {!isPublishing && isSaving && !team?.collaborativeEditing && ( - {t("Saving")}… - )} - {!isDeleted && } - {(isEditing || team?.collaborativeEditing) && !isTemplate && isNew && ( - - - - )} - {!isEditing && !isDeleted && (!isMobile || !isTemplate) && ( - - - - )} - {isEditing && ( - <> + + {!isPublishing && isSaving && !team?.collaborativeEditing && ( + {t("Saving")}… + )} + {!isDeleted && } + {(isEditing || team?.collaborativeEditing) && + !isTemplate && + isNew && ( + + + + )} + {!isEditing && !isDeleted && (!isMobile || !isTemplate) && ( + + + + )} + {isEditing && ( + <> + + + + + + + )} + {canEdit && !team?.collaborativeEditing && editAction} + {canEdit && can.createChildDocument && !isMobile && ( + + + + )} + {canEdit && isTemplate && !isDraft && !isRevision && ( + + + + )} + {can.update && isDraft && !isRevision && ( - - )} - {canEdit && !team?.collaborativeEditing && editAction} - {canEdit && can.createChildDocument && !isMobile && ( - - ( - - - - )} - /> - - )} - {canEdit && isTemplate && !isDraft && !isRevision && ( - - - - )} - {can.update && isDraft && !isRevision && ( - - - - - - )} - {!isEditing && ( - <> - {!isDeleted && } - - ( -