From 5977fe4caabff7e0d874796e79be3b0e440d1ea4 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 13 Mar 2022 22:08:26 -0700 Subject: [PATCH] fix: Editor title does not autoFocus on first load (#3238) * fix: Editor title does not autoFocus on first load * Detect IntersectionObserver for IE support --- app/components/ContentEditable.tsx | 13 +++++++++--- app/hooks/useOnScreen.ts | 34 ++++++++++++++++++++++++++++++ app/hooks/usePageVisibility.ts | 2 +- 3 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 app/hooks/useOnScreen.ts diff --git a/app/components/ContentEditable.tsx b/app/components/ContentEditable.tsx index 984446ea0..4b127a2cf 100644 --- a/app/components/ContentEditable.tsx +++ b/app/components/ContentEditable.tsx @@ -1,6 +1,7 @@ import isPrintableKeyEvent from "is-printable-key-event"; import * as React from "react"; import styled from "styled-components"; +import useOnScreen from "~/hooks/useOnScreen"; type Props = Omit, "ref" | "onChange"> & { disabled?: boolean; @@ -69,11 +70,17 @@ const ContentEditable = React.forwardRef( callback?.(event); }; - React.useLayoutEffect(() => { - if (autoFocus) { + // This is to account for being within a React.Suspense boundary, in this + // case the component may be rendered with display: none. React 18 may solve + // this in the future by delaying useEffect hooks: + // https://github.com/facebook/react/issues/14536#issuecomment-861980492 + const isVisible = useOnScreen(ref); + + React.useEffect(() => { + if (autoFocus && isVisible && !disabled && !readOnly) { ref.current?.focus(); } - }, [autoFocus, ref]); + }, [autoFocus, disabled, isVisible, readOnly, ref]); React.useEffect(() => { if (value !== ref.current?.innerText) { diff --git a/app/hooks/useOnScreen.ts b/app/hooks/useOnScreen.ts new file mode 100644 index 000000000..e73fcc357 --- /dev/null +++ b/app/hooks/useOnScreen.ts @@ -0,0 +1,34 @@ +import * as React from "react"; + +/** + * Hook to return if a given ref is visible on screen. + * + * @returns boolean if the node is visible + */ +export default function useOnScreen(ref: React.RefObject) { + const isSupported = "IntersectionObserver" in window; + const [isIntersecting, setIntersecting] = React.useState(!isSupported); + + React.useEffect(() => { + const element = ref.current; + let observer: IntersectionObserver | undefined; + + if (isSupported) { + observer = new IntersectionObserver(([entry]) => { + // Update our state when observer callback fires + setIntersecting(entry.isIntersecting); + }); + } + + if (element) { + observer?.observe(element); + } + return () => { + if (element) { + observer?.unobserve(element); + } + }; + }, []); + + return isIntersecting; +} diff --git a/app/hooks/usePageVisibility.ts b/app/hooks/usePageVisibility.ts index b5a0c35c9..1c5db9fef 100644 --- a/app/hooks/usePageVisibility.ts +++ b/app/hooks/usePageVisibility.ts @@ -1,10 +1,10 @@ import * as React from "react"; + /** * Hook to return page visibility state. * * @returns boolean if the page is visible */ - export default function usePageVisibility(): boolean { const [visible, setVisible] = React.useState(true);