Files
outline/app/scenes/Document/components/ShareButton.tsx
T
Tom Moor b70950627e Preload share popover data on hover (#11909)
* Preload share popover data on hover with useShareDataLoader hook

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: Route programmatic closes through handleOpenChange and fix race conditions

- closePopover now calls handleOpenChange(false) so reset() fires on all
  close paths, including programmatic closes via onRequestClose
- Reset requestedRef when entity id changes so preload fires for new targets
- Use request counter to prevent stale loading state when reset() is called
  during an in-flight request

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 19:44:37 -04:00

88 lines
2.3 KiB
TypeScript

import { observer } from "mobx-react";
import { GlobeIcon } from "outline-icons";
import { Suspense, useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import type Document from "~/models/Document";
import Button from "~/components/Button";
import {
Popover,
PopoverTrigger,
PopoverContent,
} from "~/components/primitives/Popover";
import useMobile from "~/hooks/useMobile";
import useShareDataLoader from "~/hooks/useShareDataLoader";
import useStores from "~/hooks/useStores";
import { preventDefault } from "~/utils/events";
import lazyWithRetry from "~/utils/lazyWithRetry";
const SharePopover = lazyWithRetry(
() => import("~/components/Sharing/Document")
);
type Props = {
/** Document being shared */
document: Document;
};
function ShareButton({ document }: Props) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const { shares } = useStores();
const isMobile = useMobile();
const share = shares.getByDocumentId(document.id);
const sharedParent = shares.getByDocumentParents(document);
const domain = share?.domain || sharedParent?.domain;
const { preload, loading, reset } = useShareDataLoader({ document });
const handleOpenChange = useCallback(
(isOpen: boolean) => {
setOpen(isOpen);
if (isOpen) {
preload();
} else {
reset();
}
},
[preload, reset]
);
const closePopover = useCallback(() => {
handleOpenChange(false);
}, [handleOpenChange]);
if (isMobile) {
return null;
}
const icon = document.isPubliclyShared ? <GlobeIcon /> : undefined;
return (
<Popover open={open} onOpenChange={handleOpenChange}>
<PopoverTrigger>
<Button icon={icon} neutral onMouseEnter={preload}>
{t("Share")} {domain && <>&middot; {domain}</>}
</Button>
</PopoverTrigger>
<PopoverContent
aria-label={t("Share")}
width={400}
minHeight={175}
side="bottom"
align="end"
onEscapeKeyDown={preventDefault}
>
<Suspense fallback={null}>
<SharePopover
document={document}
onRequestClose={closePopover}
visible={open}
loading={loading}
/>
</Suspense>
</PopoverContent>
</Popover>
);
}
export default observer(ShareButton);