From 5f788012db9ffa15962267bbbc922cef11d6ce71 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 4 Sep 2023 22:10:27 -0400 Subject: [PATCH] Hide document UI while typing --- app/components/DocumentContext.ts | 18 ++++++++++++++++++ app/components/Header.tsx | 4 +++- app/hooks/useIdle.ts | 14 +++++++++----- app/scenes/Document/components/Header.tsx | 13 +++++++++++-- .../components/KeyboardShortcutsButton.tsx | 8 ++++++-- app/scenes/Settings/Preferences.tsx | 2 +- shared/i18n/locales/en_US/translation.json | 2 +- 7 files changed, 49 insertions(+), 12 deletions(-) diff --git a/app/components/DocumentContext.ts b/app/components/DocumentContext.ts index dcbf2fed8..874d72e14 100644 --- a/app/components/DocumentContext.ts +++ b/app/components/DocumentContext.ts @@ -1,5 +1,6 @@ import * as React from "react"; import { Editor } from "~/editor"; +import useIdle from "~/hooks/useIdle"; export type DocumentContextValue = { /** The current editor instance for this document. */ @@ -16,4 +17,21 @@ const DocumentContext = React.createContext({ export const useDocumentContext = () => React.useContext(DocumentContext); +const activityEvents = [ + "click", + "mousemove", + "DOMMouseScroll", + "mousewheel", + "mousedown", + "touchstart", + "touchmove", + "focus", +]; + +export const useEditingFocus = () => { + const { editor } = useDocumentContext(); + const isIdle = useIdle(3000, activityEvents); + return isIdle && !!editor?.view.hasFocus(); +}; + export default DocumentContext; diff --git a/app/components/Header.tsx b/app/components/Header.tsx index e86e634be..f62bdbe49 100644 --- a/app/components/Header.tsx +++ b/app/components/Header.tsx @@ -21,9 +21,10 @@ type Props = { title: React.ReactNode; actions?: React.ReactNode; hasSidebar?: boolean; + className?: string; }; -function Header({ left, title, actions, hasSidebar }: Props) { +function Header({ left, title, actions, hasSidebar, className }: Props) { const { ui } = useStores(); const isMobile = useMobile(); const hasMobileSidebar = hasSidebar && isMobile; @@ -54,6 +55,7 @@ function Header({ left, title, actions, hasSidebar }: Props) { diff --git a/app/hooks/useIdle.ts b/app/hooks/useIdle.ts index a1452962f..e912b252d 100644 --- a/app/hooks/useIdle.ts +++ b/app/hooks/useIdle.ts @@ -17,10 +17,14 @@ const activityEvents = [ /** * Hook to detect user idle state. * - * @param {number} timeToIdle + * @param timeToIdle The time in ms until idle + * @param events The events to listen to * @returns boolean if the user is idle */ -export default function useIdle(timeToIdle: number = 3 * Minute) { +export default function useIdle( + timeToIdle: number = 3 * Minute, + events = activityEvents +) { const [isIdle, setIsIdle] = React.useState(false); const timeout = React.useRef>(); @@ -40,15 +44,15 @@ export default function useIdle(timeToIdle: number = 3 * Minute) { onActivity(); }, 1000); - activityEvents.forEach((eventName) => + events.forEach((eventName) => window.addEventListener(eventName, handleUserActivityEvent) ); return () => { - activityEvents.forEach((eventName) => + events.forEach((eventName) => window.removeEventListener(eventName, handleUserActivityEvent) ); }; - }, [onActivity]); + }, [events, onActivity]); return isIdle; } diff --git a/app/scenes/Document/components/Header.tsx b/app/scenes/Document/components/Header.tsx index 3a47545de..7056abe55 100644 --- a/app/scenes/Document/components/Header.tsx +++ b/app/scenes/Document/components/Header.tsx @@ -20,6 +20,7 @@ import Badge from "~/components/Badge"; import Button from "~/components/Button"; import Collaborators from "~/components/Collaborators"; import DocumentBreadcrumb from "~/components/DocumentBreadcrumb"; +import { useEditingFocus } from "~/components/DocumentContext"; import Header from "~/components/Header"; import EmojiIcon from "~/components/Icons/EmojiIcon"; import Star from "~/components/Star"; @@ -88,6 +89,7 @@ function DocumentHeader({ const { team, user } = auth; const isMobile = useMobile(); const isRevision = !!revision; + const isEditingFocus = useEditingFocus(); // 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 @@ -168,7 +170,8 @@ function DocumentHeader({ if (shareId) { return ( -
-
` + transition: opacity 500ms ease-in-out; + ${(props) => props.$hidden && "opacity: 0;"} +`; + const ArchivedBadge = styled(Badge)` position: absolute; `; diff --git a/app/scenes/Document/components/KeyboardShortcutsButton.tsx b/app/scenes/Document/components/KeyboardShortcutsButton.tsx index 1eedf252c..53fed1157 100644 --- a/app/scenes/Document/components/KeyboardShortcutsButton.tsx +++ b/app/scenes/Document/components/KeyboardShortcutsButton.tsx @@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next"; import styled from "styled-components"; import breakpoint from "styled-components-breakpoint"; import KeyboardShortcuts from "~/scenes/KeyboardShortcuts"; +import { useEditingFocus } from "~/components/DocumentContext"; import NudeButton from "~/components/NudeButton"; import Tooltip from "~/components/Tooltip"; import useStores from "~/hooks/useStores"; @@ -12,6 +13,7 @@ import useStores from "~/hooks/useStores"; function KeyboardShortcutsButton() { const { t } = useTranslation(); const { dialogs } = useStores(); + const isEditingFocus = useEditingFocus(); const handleOpenKeyboardShortcuts = () => { dialogs.openGuide({ @@ -22,18 +24,20 @@ function KeyboardShortcutsButton() { return ( - ); } -const Button = styled(NudeButton)` +const Button = styled(NudeButton)<{ $hidden: boolean }>` display: none; position: fixed; bottom: 0; margin: 24px; + transition: opacity 500ms ease-in-out; + ${(props) => props.$hidden && "opacity: 0;"} ${breakpoint("tablet")` display: block; diff --git a/app/scenes/Settings/Preferences.tsx b/app/scenes/Settings/Preferences.tsx index 62008ae30..6d4248539 100644 --- a/app/scenes/Settings/Preferences.tsx +++ b/app/scenes/Settings/Preferences.tsx @@ -122,7 +122,7 @@ function Preferences() { name={UserPreference.SeamlessEdit} label={t("Separate editing")} description={t( - `When enabled documents have a separate editing mode, when disabled documents are always editable when you have permission.` + `When enabled, documents have a separate editing mode. When disabled, documents are always editable when you have permission.` )} >