mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -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;
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user