Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: preload documents on hover #8110

Merged
merged 3 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"check:deps": "pnpm --recursive --parallel exec depcheck",
"check:format": "prettier . --check",
"check:lint": "turbo run lint --continue -- --quiet",
"check:react-compiler": "eslint --cache --no-inline-config --no-eslintrc --ext .cjs,.mjs,.js,.jsx,.ts,.tsx --parser @typescript-eslint/parser --plugin react-compiler --rule 'react-compiler/react-compiler: [warn]' --ignore-path .eslintignore.react-compiler --max-warnings 27 .",
"check:react-compiler": "eslint --cache --no-inline-config --no-eslintrc --ext .cjs,.mjs,.js,.jsx,.ts,.tsx --parser @typescript-eslint/parser --plugin react-compiler --rule 'react-compiler/react-compiler: [warn]' --ignore-path .eslintignore.react-compiler --max-warnings 26 .",
"report:react-compiler-bailout": "eslint --cache --no-inline-config --no-eslintrc --ext .cjs,.mjs,.js,.jsx,.ts,.tsx --parser @typescript-eslint/parser --plugin react-compiler --rule 'react-compiler/react-compiler: [error,{__unstable_donotuse_reportAllBailouts:true}]' --ignore-path .eslintignore.react-compiler -f ./scripts/reactCompilerBailouts.cjs . || true",
"check:test": "run-s test -- --silent",
"check:types": "tsc && turbo run check:types --filter='./packages/*' --filter='./packages/@sanity/*'",
Expand Down
4 changes: 2 additions & 2 deletions packages/sanity/src/structure/components/pane/Pane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {
type HTMLProps,
type ReactNode,
useCallback,
useEffect,
useImperativeHandle,
useLayoutEffect,
useMemo,
useRef,
useState,
Expand Down Expand Up @@ -89,7 +89,7 @@ export const Pane = forwardRef(function Pane(
ref.current = refValue
}, [])

useEffect(() => {
useLayoutEffect(() => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes a long standing quirk where clicking on a document sees a weird initial layout shift before the panel is mounted 😮‍💨

if (!rootElement) return undefined
return mount(rootElement, {
currentMinWidth: currentMinWidthProp,
Expand Down
41 changes: 31 additions & 10 deletions packages/sanity/src/structure/components/paneItem/PaneItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import {Box, type CardProps, Text} from '@sanity/ui'
import {
type ComponentType,
type MouseEvent,
type ReactNode,
startTransition,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import {
Expand All @@ -22,6 +23,7 @@ import {
SanityDefaultPreview,
useDocumentPresence,
useDocumentPreviewStore,
useEditState,
useSchema,
} from 'sanity'

Expand Down Expand Up @@ -124,14 +126,6 @@ export function PaneItem(props: PaneItemProps) {
documentPresence,
])

const Link = useMemo(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defining components during render should be avoided whenever possible 🙌

() =>
function LinkComponent(linkProps: {children: ReactNode}) {
return <ChildLink {...linkProps} childId={id} />
},
[ChildLink, id],
)

const handleClick = useCallback((e: MouseEvent<HTMLElement>) => {
if (e.metaKey) {
setClicked(false)
Expand All @@ -144,16 +138,31 @@ export function PaneItem(props: PaneItemProps) {
// Reset `clicked` state when `selected` prop changes
useEffect(() => setClicked(false), [selected])

// Preloads the edit state on hover, using concurrent rendering with `startTransition` so preloads can be interrupted and not block rendering
const [preloading, setPreload] = useState(false)
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const handleMouseEnter = useCallback(() => {
timeoutRef.current = setTimeout(() => startTransition(() => setPreload(true)), 400)
}, [])
const handleMouseLeave = useCallback(() => {
if (timeoutRef.current) clearTimeout(timeoutRef.current)
startTransition(() => setPreload(false))
}, [])

return (
<PreviewCard
data-testid={`pane-item-${title}`}
__unstable_focusRing
as={Link as FIXME}
as={ChildLink as FIXME}
// @ts-expect-error - `childId` is a valid prop on `ChildLink`
childId={id}
bjoerge marked this conversation as resolved.
Show resolved Hide resolved
data-as="a"
margin={margin}
marginBottom={marginBottom}
marginTop={marginTop}
onClick={handleClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
pressed={pressed}
radius={2}
selected={selected || clicked}
Expand All @@ -162,6 +171,18 @@ export function PaneItem(props: PaneItemProps) {
tone="inherit"
>
{preview}
{preloading && schemaType?.name && value && isSanityDocument(value) && (
<PreloadDocumentPane documentId={id} documentType={schemaType.name} />
)}
</PreviewCard>
)
}

function PreloadDocumentPane(props: {documentId: string; documentType: string}) {
const {documentId, documentType} = props
// Preload the edit state for the document, and keep it alive until mouse leave
useEditState(documentId, documentType)

return null
}
PreloadDocumentPane.displayName = 'PreloadDocumentPane'
Loading