fix: Use singleton for tooltips, ensures that only one is visible at a time. (#8069)

* fix: Use singleton for tooltips, ensures that only one is visible at a time and animations are shared

* fix: give toolbar menu its own context

* Remove duplicate props
This commit is contained in:
Tom Moor
2024-12-05 19:10:12 -05:00
committed by GitHub
parent 1154432924
commit 313b046e4e
31 changed files with 133 additions and 156 deletions
+1 -6
View File
@@ -144,12 +144,7 @@ function DocumentCard(props: Props) {
</Heading>
<DocumentMeta size="xsmall">
<Clock size={18} />
<Time
dateTime={document.updatedAt}
tooltipDelay={500}
addSuffix
shorten
/>
<Time dateTime={document.updatedAt} addSuffix shorten />
</DocumentMeta>
</div>
</Content>
+1 -5
View File
@@ -111,11 +111,7 @@ function DocumentListItem(
<Badge yellow>{t("New")}</Badge>
)}
{document.isDraft && showDraft && (
<Tooltip
content={t("Only visible to you")}
delay={500}
placement="top"
>
<Tooltip content={t("Only visible to you")} placement="top">
<Badge>{t("Draft")}</Badge>
</Tooltip>
)}
-1
View File
@@ -140,7 +140,6 @@ const EventListItem = ({ event, latest, document, ...rest }: Props) => {
title={
<Time
dateTime={event.createdAt}
tooltipDelay={500}
format={{
en_US: "MMM do, h:mm a",
fr_FR: "'Le 'd MMMM 'à' H:mm",
+1 -3
View File
@@ -23,7 +23,6 @@ function eachMinute(fn: () => void) {
export type Props = {
children?: React.ReactNode;
dateTime: string;
tooltipDelay?: number;
addSuffix?: boolean;
shorten?: boolean;
relative?: boolean;
@@ -37,7 +36,6 @@ const LocaleTime: React.FC<Props> = ({
shorten,
format,
relative,
tooltipDelay,
}: Props) => {
const userLocale = useUserLocale();
const dateFormatLong: Record<string, string> = {
@@ -82,7 +80,7 @@ const LocaleTime: React.FC<Props> = ({
});
return (
<Tooltip content={tooltipContent} delay={tooltipDelay} placement="bottom">
<Tooltip content={tooltipContent} placement="bottom">
<time dateTime={dateTime}>{children || content}</time>
</Tooltip>
);
@@ -52,11 +52,7 @@ function NotificationListItem({ notification, onNavigate }: Props) {
<Text weight="bold">{notification.subject}</Text>
</Text>
<Text type="tertiary" size="xsmall">
<Time
dateTime={notification.createdAt}
tooltipDelay={1000}
addSuffix
/>{" "}
<Time dateTime={notification.createdAt} addSuffix />{" "}
{collection && <>&middot; {collection.name}</>}
</Text>
{notification.comment && (
@@ -60,7 +60,7 @@ function Notifications(
</Text>
<Flex gap={8}>
{notifications.approximateUnreadCount > 0 && (
<Tooltip delay={500} content={t("Mark all as read")}>
<Tooltip content={t("Mark all as read")}>
<Button action={markNotificationsAsRead} context={context}>
<MarkAsReadIcon />
</Button>
+1 -1
View File
@@ -128,7 +128,7 @@ const Reaction: React.FC<Props> = ({
);
return tooltipContent ? (
<Tooltip content={tooltipContent} delay={250} placement="bottom">
<Tooltip content={tooltipContent} placement="bottom">
{DisplayedEmoji}
</Tooltip>
) : (
+1 -6
View File
@@ -98,12 +98,7 @@ const ReactionPicker: React.FC<Props> = ({
<>
<PopoverDisclosure {...popover}>
{(props) => (
<Tooltip
content={t("Add reaction")}
placement="top"
delay={500}
hideOnClick
>
<Tooltip content={t("Add reaction")} placement="top" hideOnClick>
<NudeButton
{...props}
aria-label={t("Reaction picker")}
@@ -119,7 +119,7 @@ function PublicAccess({ document, share, sharedParent }: Props) {
: share?.url ?? "";
const copyButton = (
<Tooltip content={t("Copy public link")} delay={500} placement="top">
<Tooltip content={t("Copy public link")} placement="top">
<CopyToClipboard text={shareUrl} onCopy={handleCopied}>
<NudeButton type="button" disabled={!share} style={{ marginRight: 3 }}>
<CopyIcon color={theme.placeholder} size={18} />
@@ -31,7 +31,7 @@ export function CopyLinkButton({
}, [onCopy, t]);
return (
<Tooltip content={t("Copy link")} delay={500} placement="top">
<Tooltip content={t("Copy link")} placement="top">
<CopyToClipboard text={url} onCopy={handleCopied}>
<NudeButton type="button">
<LinkIcon size={20} />
-1
View File
@@ -80,7 +80,6 @@ function AppSidebar() {
<Tooltip
content={t("Toggle sidebar")}
shortcut={`${metaDisplay}+.`}
delay={500}
>
<ToggleButton
position="bottom"
+1 -5
View File
@@ -42,11 +42,7 @@ function SettingsSidebar() {
image={<StyledBackIcon />}
onClick={returnToApp}
>
<Tooltip
content={t("Toggle sidebar")}
shortcut={`${metaDisplay}+.`}
delay={500}
>
<Tooltip content={t("Toggle sidebar")} shortcut={`${metaDisplay}+.`}>
<ToggleButton
position="bottom"
image={<SidebarIcon />}
+1 -5
View File
@@ -81,11 +81,7 @@ const ToggleSidebar = () => {
const { ui } = useStores();
return (
<Tooltip
content={t("Toggle sidebar")}
shortcut={`${metaDisplay}+.`}
delay={500}
>
<Tooltip content={t("Toggle sidebar")} shortcut={`${metaDisplay}+.`}>
<ToggleButton
position="bottom"
image={<SidebarIcon />}
@@ -278,7 +278,7 @@ function InnerDocumentLink(
!isDraggingAnyDocument ? (
<Fade>
{can.createChildDocument && (
<Tooltip content={t("New doc")} delay={500}>
<Tooltip content={t("New doc")}>
<NudeButton
type={undefined}
aria-label={t("New nested document")}
@@ -43,12 +43,12 @@ function HistoryNavigation(props: React.ComponentProps<typeof Flex>) {
return (
<Navigation gap={4} {...props}>
<Tooltip content={t("Go back")} delay={500}>
<Tooltip content={t("Go back")}>
<NudeButton onClick={() => Desktop.bridge?.goBack()}>
<Back $active={back} />
</NudeButton>
</Tooltip>
<Tooltip content={t("Go forward")} delay={500}>
<Tooltip content={t("Go forward")}>
<NudeButton onClick={() => Desktop.bridge?.goForward()}>
<Forward $active={forward} />
</NudeButton>
+1 -1
View File
@@ -22,7 +22,7 @@ function Time({ onClick, ...props }: Props) {
<time dateTime={props.dateTime}>{props.children || content}</time>
}
>
<LocaleTime tooltipDelay={250} {...props} />
<LocaleTime {...props} />
</React.Suspense>
</span>
);
+5 -12
View File
@@ -1,9 +1,9 @@
import Tippy, { TippyProps } from "@tippyjs/react";
import * as React from "react";
import styled, { createGlobalStyle } from "styled-components";
import { roundArrow } from "tippy.js";
import { s } from "@shared/styles";
import useMobile from "~/hooks/useMobile";
import { useTooltipContext } from "./TooltipContext";
export type Props = Omit<TippyProps, "content" | "theme"> & {
/** The content to display in the tooltip. */
@@ -12,8 +12,9 @@ export type Props = Omit<TippyProps, "content" | "theme"> & {
shortcut?: React.ReactNode;
};
function Tooltip({ shortcut, content: tooltip, delay = 50, ...rest }: Props) {
function Tooltip({ shortcut, content: tooltip, delay = 500, ...rest }: Props) {
const isMobile = useMobile();
const singleton = useTooltipContext();
let content = <>{tooltip}</>;
@@ -30,15 +31,7 @@ function Tooltip({ shortcut, content: tooltip, delay = 50, ...rest }: Props) {
}
return (
<Tippy
arrow={roundArrow}
animation="shift-away"
content={content}
delay={delay}
duration={[200, 150]}
inertia
{...rest}
/>
<Tippy content={content} delay={delay} singleton={singleton} {...rest} />
);
}
@@ -132,7 +125,7 @@ export const TooltipStyles = createGlobalStyle`
padding:5px 9px;
z-index:1
}
/* Arrow Styles */
.tippy-box[data-placement^=top]>.tippy-svg-arrow{
bottom:0
+36
View File
@@ -0,0 +1,36 @@
import Tippy, { useSingleton, TippyProps } from "@tippyjs/react";
import * as React from "react";
import { roundArrow } from "tippy.js";
export const TooltipContext =
React.createContext<TippyProps["singleton"]>(undefined);
export function useTooltipContext() {
return React.useContext(TooltipContext);
}
type Props = {
children: React.ReactNode;
tippyProps?: TippyProps;
};
export function TooltipProvider({ children, tippyProps }: Props) {
const [source, target] = useSingleton();
return (
<>
<Tippy
delay={500}
arrow={roundArrow}
animation="shift-away"
singleton={source}
duration={[200, 150]}
inertia
{...tippyProps}
/>
<TooltipContext.Provider value={target}>
{children}
</TooltipContext.Provider>
</>
);
}
+2 -14
View File
@@ -286,7 +286,6 @@ export default function FindAndReplace({
<Tooltip
content={t("Previous match")}
shortcut="shift+enter"
delay={500}
placement="bottom"
>
<ButtonLarge
@@ -296,12 +295,7 @@ export default function FindAndReplace({
<CaretUpIcon />
</ButtonLarge>
</Tooltip>
<Tooltip
content={t("Next match")}
shortcut="enter"
delay={500}
placement="bottom"
>
<Tooltip content={t("Next match")} shortcut="enter" placement="bottom">
<ButtonLarge
disabled={disabled}
onClick={() => editor.commands.nextSearchMatch()}
@@ -336,7 +330,6 @@ export default function FindAndReplace({
<Tooltip
content={t("Match case")}
shortcut={`${altDisplay}+${metaDisplay}+c`}
delay={500}
placement="bottom"
>
<ButtonSmall onClick={handleCaseSensitive}>
@@ -348,7 +341,6 @@ export default function FindAndReplace({
<Tooltip
content={t("Enable regex")}
shortcut={`${altDisplay}+${metaDisplay}+r`}
delay={500}
placement="bottom"
>
<ButtonSmall onClick={handleRegex}>
@@ -361,11 +353,7 @@ export default function FindAndReplace({
</StyledInput>
{navigation}
{!readOnly && (
<Tooltip
content={t("Replace options")}
delay={500}
placement="bottom"
>
<Tooltip content={t("Replace options")} placement="bottom">
<ButtonLarge onClick={handleMore}>
<ReplaceIcon color={theme.textSecondary} />
</ButtonLarge>
+35 -29
View File
@@ -1,3 +1,4 @@
import { TippyProps } from "@tippyjs/react";
import * as React from "react";
import { useMenuState } from "reakit";
import { MenuButton } from "reakit/Menu";
@@ -7,6 +8,7 @@ import { MenuItem } from "@shared/editor/types";
import { s } from "@shared/styles";
import ContextMenu from "~/components/ContextMenu";
import Template from "~/components/ContextMenu/Template";
import { TooltipProvider } from "~/components/TooltipContext";
import { MenuItem as TMenuItem } from "~/types";
import { useEditor } from "./EditorContext";
import ToolbarButton from "./ToolbarButton";
@@ -75,6 +77,8 @@ function ToolbarDropdown(props: { active: boolean; item: MenuItem }) {
);
}
const tippyProps = { placement: "top" } as TippyProps;
function ToolbarMenu(props: Props) {
const { commands, view } = useEditor();
const { items } = props;
@@ -91,36 +95,38 @@ function ToolbarMenu(props: Props) {
};
return (
<FlexibleWrapper>
{items.map((item, index) => {
if (item.name === "separator" && item.visible !== false) {
return <ToolbarSeparator key={index} />;
}
if (item.visible === false || !item.icon) {
return null;
}
const isActive = item.active ? item.active(state) : false;
<TooltipProvider tippyProps={tippyProps}>
<FlexibleWrapper>
{items.map((item, index) => {
if (item.name === "separator" && item.visible !== false) {
return <ToolbarSeparator key={index} />;
}
if (item.visible === false || !item.icon) {
return null;
}
const isActive = item.active ? item.active(state) : false;
return (
<Tooltip
content={item.label === item.tooltip ? undefined : item.tooltip}
key={index}
>
{item.children ? (
<ToolbarDropdown active={isActive && !item.label} item={item} />
) : (
<ToolbarButton
onClick={handleClick(item)}
active={isActive && !item.label}
>
{item.label && <Label>{item.label}</Label>}
{item.icon}
</ToolbarButton>
)}
</Tooltip>
);
})}
</FlexibleWrapper>
return (
<Tooltip
content={item.label === item.tooltip ? undefined : item.tooltip}
key={index}
>
{item.children ? (
<ToolbarDropdown active={isActive && !item.label} item={item} />
) : (
<ToolbarButton
onClick={handleClick(item)}
active={isActive && !item.label}
>
{item.label && <Label>{item.label}</Label>}
{item.icon}
</ToolbarButton>
)}
</Tooltip>
);
})}
</FlexibleWrapper>
</TooltipProvider>
);
}
+12 -2
View File
@@ -8,8 +8,18 @@ type Props = {
children?: React.ReactNode;
};
const WrappedTooltip: React.FC<Props> = ({ children, content }: Props) => (
<Tooltip offset={[0, 16]} delay={150} content={content} placement="top">
const WrappedTooltip: React.FC<Props> = ({
children,
content,
...rest
}: Props) => (
<Tooltip
offset={[0, 16]}
delay={150}
content={content}
placement="top"
{...rest}
>
<TooltipContent>{children}</TooltipContent>
</Tooltip>
);
+18 -15
View File
@@ -20,6 +20,7 @@ import { initI18n } from "~/utils/i18n";
import Desktop from "./components/DesktopEventHandler";
import LazyPolyfill from "./components/LazyPolyfills";
import PageScroll from "./components/PageScroll";
import { TooltipProvider } from "./components/TooltipContext";
import Routes from "./routes";
import Logger from "./utils/Logger";
import { PluginManager } from "./utils/PluginManager";
@@ -55,21 +56,23 @@ if (element) {
<Theme>
<ErrorBoundary showTitle>
<KBarProvider actions={[]} options={commandBarOptions}>
<LazyPolyfill>
<LazyMotion features={loadFeatures}>
<Router history={history}>
<PageScroll>
<PageTheme />
<ScrollToTop>
<Routes />
</ScrollToTop>
<Toasts />
<Dialogs />
<Desktop />
</PageScroll>
</Router>
</LazyMotion>
</LazyPolyfill>
<TooltipProvider>
<LazyPolyfill>
<LazyMotion features={loadFeatures}>
<Router history={history}>
<PageScroll>
<PageTheme />
<ScrollToTop>
<Routes />
</ScrollToTop>
<Toasts />
<Dialogs />
<Desktop />
</PageScroll>
</Router>
</LazyMotion>
</LazyPolyfill>
</TooltipProvider>
</KBarProvider>
</ErrorBoundary>
</Theme>
+1 -6
View File
@@ -19,12 +19,7 @@ function NewDocumentMenu() {
}
return (
<Tooltip
content={t("New document")}
shortcut="n"
delay={500}
placement="bottom"
>
<Tooltip content={t("New document")} shortcut="n" placement="bottom">
<Button as={Link} to={newDocumentPath()} icon={<PlusIcon />}>
{t("New doc")}
</Button>
@@ -27,7 +27,6 @@ function Actions({ collection }: Props) {
<Tooltip
content={t("New document")}
shortcut="n"
delay={500}
placement="bottom"
>
<Button
@@ -316,7 +316,7 @@ function CommentForm({
{t("Cancel")}
</ButtonSmall>
</Flex>
<Tooltip delay={500} content={t("Upload image")} placement="top">
<Tooltip content={t("Upload image")} placement="top">
<NudeButton onClick={handleImageUpload}>
<ImageIcon color={theme.textTertiary} />
</NudeButton>
@@ -197,21 +197,12 @@ function CommentThreadItem({
{showAuthor && <em>{comment.createdBy.name}</em>}
{showAuthor && showTime && <> &middot; </>}
{showTime && (
<Time
dateTime={comment.createdAt}
tooltipDelay={500}
addSuffix
shorten
/>
<Time dateTime={comment.createdAt} addSuffix shorten />
)}
{showEdited && (
<>
{" "}
(
<Time dateTime={comment.updatedAt} tooltipDelay={500}>
{t("edited")}
</Time>
)
(<Time dateTime={comment.updatedAt}>{t("edited")}</Time>)
</>
)}
</Meta>
@@ -304,12 +295,7 @@ const ResolveButton = ({
const { t } = useTranslation();
return (
<Tooltip
content={t("Mark as resolved")}
placement="top"
delay={500}
hideOnClick
>
<Tooltip content={t("Mark as resolved")} placement="top" hideOnClick>
<Action
as={NudeButton}
context={context}
+1 -10
View File
@@ -133,7 +133,6 @@ function DocumentHeader({
: `${t("Show contents")} (${t("available when headings are added")})`
}
shortcut={`ctrl+${altDisplay}+h`}
delay={250}
placement="bottom"
>
<Button
@@ -151,7 +150,6 @@ function DocumentHeader({
noun: document.noun,
})}
shortcut="e"
delay={500}
placement="bottom"
>
<Button
@@ -171,7 +169,6 @@ function DocumentHeader({
content={
resolvedTheme === "light" ? t("Switch to dark") : t("Switch to light")
}
delay={500}
placement="bottom"
>
<Button
@@ -290,7 +287,6 @@ function DocumentHeader({
<Tooltip
content={t("Save")}
shortcut={`${metaDisplay}+enter`}
delay={500}
placement="bottom"
>
<Button
@@ -323,7 +319,6 @@ function DocumentHeader({
<Tooltip
content={t("New document")}
shortcut="n"
delay={500}
placement="bottom"
>
<Button icon={<PlusIcon />} {...props} neutral>
@@ -336,11 +331,7 @@ function DocumentHeader({
)}
{revision && revision.createdAt !== document.updatedAt && (
<Action>
<Tooltip
content={t("Restore version")}
delay={500}
placement="bottom"
>
<Tooltip content={t("Restore version")} placement="bottom">
<Button
action={restoreRevision}
context={context}
@@ -23,7 +23,7 @@ function KeyboardShortcutsButton() {
};
return (
<Tooltip content={t("Keyboard shortcuts")} shortcut="?" delay={500}>
<Tooltip content={t("Keyboard shortcuts")} shortcut="?">
<Button onClick={handleOpenKeyboardShortcuts} $hidden={isEditingFocus}>
<KeyboardIcon />
</Button>
@@ -32,7 +32,7 @@ function SidebarLayout({ title, onClose, children, scrollable = true }: Props) {
<>
<Header>
<Title>{title}</Title>
<Tooltip content={t("Close")} shortcut="Esc" delay={500}>
<Tooltip content={t("Close")} shortcut="Esc">
<Button
icon={<ForwardIcon />}
onClick={onClose}
@@ -17,7 +17,7 @@ export function DocumentFilter(props: Props) {
return (
<div>
<Tooltip content={t("Remove document filter")} delay={350}>
<Tooltip content={t("Remove document filter")}>
<StyledButton onClick={props.onClick} icon={<CloseIcon />} neutral>
{props.document.title}
</StyledButton>
@@ -33,7 +33,7 @@ function RecentSearchListItem({ searchQuery }: Props) {
{...rovingTabIndex}
>
{searchQuery.query}
<Tooltip content={t("Remove search")} delay={150}>
<Tooltip content={t("Remove search")}>
<RemoveButton
aria-label={t("Remove search")}
onClick={async (ev) => {