Lazy-load the date picker to keep Radix out of the editor schema graph

Importing @radix-ui/react-popover and react-day-picker at the top of
Mentions.tsx pulled them into the editor schema's static import graph,
which is also loaded on the server. Radix's prebuilt ESM does a bare
"react/jsx-runtime" import that the node/shared test resolvers can't
resolve, breaking all server and shared editor test suites.

Move the popover + calendar into DateMentionPicker, loaded via
React.lazy, so the browser-only dependencies are code-split out of the
schema graph and only fetched when an editable date mention renders.
This commit is contained in:
Claude
2026-06-08 02:11:03 +00:00
parent 249debadf2
commit 83a0b63a52
2 changed files with 113 additions and 69 deletions
@@ -0,0 +1,100 @@
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { Slot } from "@radix-ui/react-slot";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { RemoveScroll } from "react-remove-scroll";
import styled from "styled-components";
import { Calendar } from "../../components/Calendar";
import { depths, s } from "../../styles";
import { dateLocale, toISODate } from "../../utils/date";
type Props = {
/** The currently selected date, if any. */
selectedDate?: Date;
/** The user's language, used to localise the calendar. */
language?: Parameters<typeof dateLocale>[0];
/** Called with the new date-only ISO string when a day is picked. */
onChange: (modelId: string) => void;
/** The trigger element the calendar popover is anchored to. */
children: React.ReactNode;
};
/**
* The interactive calendar popover for a date mention. It lives in its own
* module so that its browser-only dependencies (Radix, react-day-picker) are
* loaded lazily and stay out of the editor schema graph, which is also imported
* on the server.
*
* @returns the popover wrapping the provided trigger.
*/
export default function DateMentionPicker({
selectedDate,
language,
onChange,
children,
}: Props) {
const { t } = useTranslation();
const [open, setOpen] = React.useState(false);
const handleSelect = React.useCallback(
(date: Date) => {
setOpen(false);
onChange(toISODate(date));
},
[onChange]
);
return (
<PopoverPrimitive.Root open={open} onOpenChange={setOpen}>
<PopoverPrimitive.Trigger
asChild
onMouseDown={(e) => e.stopPropagation()}
>
{children}
</PopoverPrimitive.Trigger>
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
asChild
sideOffset={4}
align="start"
aria-label={t("Choose a date")}
onOpenAutoFocus={(e) => e.preventDefault()}
>
<RemoveScroll as={Slot} allowPinchZoom>
<DatePopoverContent>
<Calendar
required
mode="single"
selected={selectedDate}
defaultMonth={selectedDate}
onSelect={handleSelect}
locale={dateLocale(language)}
/>
</DatePopoverContent>
</RemoveScroll>
</PopoverPrimitive.Content>
</PopoverPrimitive.Portal>
</PopoverPrimitive.Root>
);
}
const DatePopoverContent = styled.div`
z-index: ${depths.modal};
background: ${s("menuBackground")};
box-shadow: ${s("menuShadow")};
border-radius: 8px;
outline: none;
&[data-state="open"] {
animation: fadeIn 150ms ease;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
`;
+13 -69
View File
@@ -1,5 +1,3 @@
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { Slot } from "@radix-ui/react-slot";
import { observer } from "mobx-react";
import {
DocumentIcon,
@@ -10,18 +8,10 @@ import {
import type { Node } from "prosemirror-model";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { RemoveScroll } from "react-remove-scroll";
import { Link } from "react-router-dom";
import styled from "styled-components";
import { depths, s } from "../../styles";
import {
dateLocale,
dateToRelativeReadable,
parseISODate,
toISODate,
} from "../../utils/date";
import { dateToRelativeReadable, parseISODate } from "../../utils/date";
import { Backticks } from "../../components/Backticks";
import { Calendar } from "../../components/Calendar";
import Flex from "../../components/Flex";
import Icon from "../../components/Icon";
import { IssueStatusIcon } from "../../components/IssueStatusIcon";
@@ -525,11 +515,15 @@ type DateProps = ComponentProps & {
onChangeDate: (modelId: string) => void;
};
// Loaded lazily so its browser-only dependencies (Radix, react-day-picker)
// don't enter the editor schema's static import graph, which is also used on
// the server.
const DateMentionPicker = React.lazy(() => import("./DateMentionPicker"));
export const MentionDate = observer(function MentionDate_(props: DateProps) {
const { isSelected, isEditable, node, onChangeDate } = props;
const { t } = useTranslation();
const { auth } = useStores();
const [open, setOpen] = React.useState(false);
const { className, unfurl, ...attrs } = getAttributesFromNode(node);
const language = auth.user?.language;
@@ -537,14 +531,6 @@ export const MentionDate = observer(function MentionDate_(props: DateProps) {
const display = dateToRelativeReadable(iso, t, language);
const selectedDate = parseISODate(iso) ?? undefined;
const handleSelect = React.useCallback(
(date: Date) => {
setOpen(false);
onChangeDate(toISODate(date));
},
[onChangeDate]
);
const content = (
<DateMention
{...attrs}
@@ -562,36 +548,15 @@ export const MentionDate = observer(function MentionDate_(props: DateProps) {
}
return (
<PopoverPrimitive.Root open={open} onOpenChange={setOpen}>
<PopoverPrimitive.Trigger
asChild
onMouseDown={(e) => e.stopPropagation()}
<React.Suspense fallback={content}>
<DateMentionPicker
selectedDate={selectedDate}
language={language}
onChange={onChangeDate}
>
{content}
</PopoverPrimitive.Trigger>
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
asChild
sideOffset={4}
align="start"
aria-label={t("Choose a date")}
onOpenAutoFocus={(e) => e.preventDefault()}
>
<RemoveScroll as={Slot} allowPinchZoom>
<DatePopoverContent>
<Calendar
required
mode="single"
selected={selectedDate}
defaultMonth={selectedDate}
onSelect={handleSelect}
locale={dateLocale(language)}
/>
</DatePopoverContent>
</RemoveScroll>
</PopoverPrimitive.Content>
</PopoverPrimitive.Portal>
</PopoverPrimitive.Root>
</DateMentionPicker>
</React.Suspense>
);
});
@@ -622,27 +587,6 @@ const DateMention = styled.span<{ $editable: boolean }>`
user-select: none;
`;
const DatePopoverContent = styled.div`
z-index: ${depths.modal};
background: ${s("menuBackground")};
box-shadow: ${s("menuShadow")};
border-radius: 8px;
outline: none;
&[data-state="open"] {
animation: fadeIn 150ms ease;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
`;
const StyledWarningIcon = styled(WarningIcon)`
margin: 0 -2px;
`;