Hide document UI while typing
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Editor } from "~/editor";
|
import { Editor } from "~/editor";
|
||||||
|
import useIdle from "~/hooks/useIdle";
|
||||||
|
|
||||||
export type DocumentContextValue = {
|
export type DocumentContextValue = {
|
||||||
/** The current editor instance for this document. */
|
/** The current editor instance for this document. */
|
||||||
@@ -16,4 +17,21 @@ const DocumentContext = React.createContext<DocumentContextValue>({
|
|||||||
|
|
||||||
export const useDocumentContext = () => React.useContext(DocumentContext);
|
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;
|
export default DocumentContext;
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ type Props = {
|
|||||||
title: React.ReactNode;
|
title: React.ReactNode;
|
||||||
actions?: React.ReactNode;
|
actions?: React.ReactNode;
|
||||||
hasSidebar?: boolean;
|
hasSidebar?: boolean;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function Header({ left, title, actions, hasSidebar }: Props) {
|
function Header({ left, title, actions, hasSidebar, className }: Props) {
|
||||||
const { ui } = useStores();
|
const { ui } = useStores();
|
||||||
const isMobile = useMobile();
|
const isMobile = useMobile();
|
||||||
const hasMobileSidebar = hasSidebar && isMobile;
|
const hasMobileSidebar = hasSidebar && isMobile;
|
||||||
@@ -54,6 +55,7 @@ function Header({ left, title, actions, hasSidebar }: Props) {
|
|||||||
<Wrapper
|
<Wrapper
|
||||||
align="center"
|
align="center"
|
||||||
shrink={false}
|
shrink={false}
|
||||||
|
className={className}
|
||||||
$passThrough={passThrough}
|
$passThrough={passThrough}
|
||||||
$insetTitleAdjust={ui.sidebarIsClosed && Desktop.hasInsetTitlebar()}
|
$insetTitleAdjust={ui.sidebarIsClosed && Desktop.hasInsetTitlebar()}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -17,10 +17,14 @@ const activityEvents = [
|
|||||||
/**
|
/**
|
||||||
* Hook to detect user idle state.
|
* 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
|
* @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 [isIdle, setIsIdle] = React.useState(false);
|
||||||
const timeout = React.useRef<ReturnType<typeof setTimeout>>();
|
const timeout = React.useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
@@ -40,15 +44,15 @@ export default function useIdle(timeToIdle: number = 3 * Minute) {
|
|||||||
onActivity();
|
onActivity();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
activityEvents.forEach((eventName) =>
|
events.forEach((eventName) =>
|
||||||
window.addEventListener(eventName, handleUserActivityEvent)
|
window.addEventListener(eventName, handleUserActivityEvent)
|
||||||
);
|
);
|
||||||
return () => {
|
return () => {
|
||||||
activityEvents.forEach((eventName) =>
|
events.forEach((eventName) =>
|
||||||
window.removeEventListener(eventName, handleUserActivityEvent)
|
window.removeEventListener(eventName, handleUserActivityEvent)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}, [onActivity]);
|
}, [events, onActivity]);
|
||||||
|
|
||||||
return isIdle;
|
return isIdle;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import Badge from "~/components/Badge";
|
|||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
import Collaborators from "~/components/Collaborators";
|
import Collaborators from "~/components/Collaborators";
|
||||||
import DocumentBreadcrumb from "~/components/DocumentBreadcrumb";
|
import DocumentBreadcrumb from "~/components/DocumentBreadcrumb";
|
||||||
|
import { useEditingFocus } from "~/components/DocumentContext";
|
||||||
import Header from "~/components/Header";
|
import Header from "~/components/Header";
|
||||||
import EmojiIcon from "~/components/Icons/EmojiIcon";
|
import EmojiIcon from "~/components/Icons/EmojiIcon";
|
||||||
import Star from "~/components/Star";
|
import Star from "~/components/Star";
|
||||||
@@ -88,6 +89,7 @@ function DocumentHeader({
|
|||||||
const { team, user } = auth;
|
const { team, user } = auth;
|
||||||
const isMobile = useMobile();
|
const isMobile = useMobile();
|
||||||
const isRevision = !!revision;
|
const isRevision = !!revision;
|
||||||
|
const isEditingFocus = useEditingFocus();
|
||||||
|
|
||||||
// We cache this value for as long as the component is mounted so that if you
|
// 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
|
// apply a template there is still the option to replace it until the user
|
||||||
@@ -168,7 +170,8 @@ function DocumentHeader({
|
|||||||
|
|
||||||
if (shareId) {
|
if (shareId) {
|
||||||
return (
|
return (
|
||||||
<Header
|
<StyledHeader
|
||||||
|
$hidden={isEditingFocus}
|
||||||
title={document.title}
|
title={document.title}
|
||||||
hasSidebar={!!sharedTree}
|
hasSidebar={!!sharedTree}
|
||||||
left={
|
left={
|
||||||
@@ -196,7 +199,8 @@ function DocumentHeader({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header
|
<StyledHeader
|
||||||
|
$hidden={isEditingFocus}
|
||||||
hasSidebar
|
hasSidebar
|
||||||
left={
|
left={
|
||||||
isMobile ? (
|
isMobile ? (
|
||||||
@@ -362,6 +366,11 @@ function DocumentHeader({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const StyledHeader = styled(Header)<{ $hidden: boolean }>`
|
||||||
|
transition: opacity 500ms ease-in-out;
|
||||||
|
${(props) => props.$hidden && "opacity: 0;"}
|
||||||
|
`;
|
||||||
|
|
||||||
const ArchivedBadge = styled(Badge)`
|
const ArchivedBadge = styled(Badge)`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import breakpoint from "styled-components-breakpoint";
|
import breakpoint from "styled-components-breakpoint";
|
||||||
import KeyboardShortcuts from "~/scenes/KeyboardShortcuts";
|
import KeyboardShortcuts from "~/scenes/KeyboardShortcuts";
|
||||||
|
import { useEditingFocus } from "~/components/DocumentContext";
|
||||||
import NudeButton from "~/components/NudeButton";
|
import NudeButton from "~/components/NudeButton";
|
||||||
import Tooltip from "~/components/Tooltip";
|
import Tooltip from "~/components/Tooltip";
|
||||||
import useStores from "~/hooks/useStores";
|
import useStores from "~/hooks/useStores";
|
||||||
@@ -12,6 +13,7 @@ import useStores from "~/hooks/useStores";
|
|||||||
function KeyboardShortcutsButton() {
|
function KeyboardShortcutsButton() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { dialogs } = useStores();
|
const { dialogs } = useStores();
|
||||||
|
const isEditingFocus = useEditingFocus();
|
||||||
|
|
||||||
const handleOpenKeyboardShortcuts = () => {
|
const handleOpenKeyboardShortcuts = () => {
|
||||||
dialogs.openGuide({
|
dialogs.openGuide({
|
||||||
@@ -22,18 +24,20 @@ function KeyboardShortcutsButton() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip tooltip={t("Keyboard shortcuts")} shortcut="?" delay={500}>
|
<Tooltip tooltip={t("Keyboard shortcuts")} shortcut="?" delay={500}>
|
||||||
<Button onClick={handleOpenKeyboardShortcuts}>
|
<Button onClick={handleOpenKeyboardShortcuts} $hidden={isEditingFocus}>
|
||||||
<KeyboardIcon />
|
<KeyboardIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button = styled(NudeButton)`
|
const Button = styled(NudeButton)<{ $hidden: boolean }>`
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
margin: 24px;
|
margin: 24px;
|
||||||
|
transition: opacity 500ms ease-in-out;
|
||||||
|
${(props) => props.$hidden && "opacity: 0;"}
|
||||||
|
|
||||||
${breakpoint("tablet")`
|
${breakpoint("tablet")`
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ function Preferences() {
|
|||||||
name={UserPreference.SeamlessEdit}
|
name={UserPreference.SeamlessEdit}
|
||||||
label={t("Separate editing")}
|
label={t("Separate editing")}
|
||||||
description={t(
|
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.`
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -834,7 +834,7 @@
|
|||||||
"Show a hand cursor when hovering over interactive elements.": "Show a hand cursor when hovering over interactive elements.",
|
"Show a hand cursor when hovering over interactive elements.": "Show a hand cursor when hovering over interactive elements.",
|
||||||
"Show line numbers": "Show line numbers",
|
"Show line numbers": "Show line numbers",
|
||||||
"Show line numbers on code blocks in documents.": "Show line numbers on code blocks in documents.",
|
"Show line numbers on code blocks in documents.": "Show line numbers on code blocks in documents.",
|
||||||
"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.",
|
"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.",
|
||||||
"Remember previous location": "Remember previous location",
|
"Remember previous location": "Remember previous location",
|
||||||
"Automatically return to the document you were last viewing when the app is re-opened.": "Automatically return to the document you were last viewing when the app is re-opened.",
|
"Automatically return to the document you were last viewing when the app is re-opened.": "Automatically return to the document you were last viewing when the app is re-opened.",
|
||||||
"You may delete your account at any time, note that this is unrecoverable": "You may delete your account at any time, note that this is unrecoverable",
|
"You may delete your account at any time, note that this is unrecoverable": "You may delete your account at any time, note that this is unrecoverable",
|
||||||
|
|||||||
Reference in New Issue
Block a user