chore: Enable recommended React eslint rules (#5600)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"extends": [
|
||||
"../.eslintrc",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
],
|
||||
"plugins": [
|
||||
|
||||
@@ -61,8 +61,11 @@ export const openDocument = createAction({
|
||||
// cache if the document is renamed
|
||||
id: path.url,
|
||||
name: path.title,
|
||||
icon: () =>
|
||||
stores.documents.get(path.id)?.isStarred ? <StarredIcon /> : null,
|
||||
icon: function _Icon() {
|
||||
return stores.documents.get(path.id)?.isStarred ? (
|
||||
<StarredIcon />
|
||||
) : null;
|
||||
},
|
||||
section: DocumentSection,
|
||||
perform: () => history.push(path.url),
|
||||
}));
|
||||
|
||||
@@ -43,8 +43,9 @@ export const changeTheme = createAction({
|
||||
isContextMenu ? t("Appearance") : t("Change theme"),
|
||||
analyticsName: "Change theme",
|
||||
placeholder: ({ t }) => t("Change theme to"),
|
||||
icon: () =>
|
||||
stores.ui.resolvedTheme === "light" ? <SunIcon /> : <MoonIcon />,
|
||||
icon: function _Icon() {
|
||||
return stores.ui.resolvedTheme === "light" ? <SunIcon /> : <MoonIcon />;
|
||||
},
|
||||
keywords: "appearance display",
|
||||
section: SettingsSection,
|
||||
children: [changeToLightTheme, changeToDarkTheme, changeToSystemTheme],
|
||||
|
||||
@@ -16,18 +16,20 @@ export const createTeamsList = ({ stores }: { stores: RootStore }) =>
|
||||
analyticsName: "Switch workspace",
|
||||
section: TeamSection,
|
||||
keywords: "change switch workspace organization team",
|
||||
icon: () => (
|
||||
<StyledTeamLogo
|
||||
alt={session.name}
|
||||
model={{
|
||||
initial: session.name[0],
|
||||
avatarUrl: session.avatarUrl,
|
||||
id: session.id,
|
||||
color: stringToColor(session.id),
|
||||
}}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
icon: function _Icon() {
|
||||
return (
|
||||
<StyledTeamLogo
|
||||
alt={session.name}
|
||||
model={{
|
||||
initial: session.name[0],
|
||||
avatarUrl: session.avatarUrl,
|
||||
id: session.id,
|
||||
color: stringToColor(session.id),
|
||||
}}
|
||||
size={24}
|
||||
/>
|
||||
);
|
||||
},
|
||||
visible: ({ currentTeamId }: ActionContext) => currentTeamId !== session.id,
|
||||
perform: () => (window.location.href = session.url),
|
||||
})) ?? [];
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import * as React from "react";
|
||||
import Tooltip, { Props as TooltipProps } from "~/components/Tooltip";
|
||||
import { Action, ActionContext } from "~/types";
|
||||
|
||||
export type Props = React.ComponentPropsWithoutRef<"button"> & {
|
||||
export type Props = React.HTMLAttributes<HTMLButtonElement> & {
|
||||
/** Show the button in a disabled state */
|
||||
disabled?: boolean;
|
||||
/** Hide the button entirely if action is not applicable */
|
||||
@@ -18,11 +19,11 @@ export type Props = React.ComponentPropsWithoutRef<"button"> & {
|
||||
/**
|
||||
* Button that can be used to trigger an action definition.
|
||||
*/
|
||||
const ActionButton = React.forwardRef(
|
||||
(
|
||||
const ActionButton = React.forwardRef<HTMLButtonElement, Props>(
|
||||
function _ActionButton(
|
||||
{ action, context, tooltip, hideOnActionDisabled, ...rest }: Props,
|
||||
ref: React.Ref<HTMLButtonElement>
|
||||
) => {
|
||||
) {
|
||||
const [executing, setExecuting] = React.useState(false);
|
||||
const disabled = rest.disabled;
|
||||
|
||||
|
||||
@@ -5,7 +5,11 @@ import * as React from "react";
|
||||
import { IntegrationService } from "@shared/types";
|
||||
import env from "~/env";
|
||||
|
||||
const Analytics: React.FC = ({ children }) => {
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const Analytics: React.FC = ({ children }: Props) => {
|
||||
// Google Analytics 3
|
||||
React.useEffect(() => {
|
||||
if (!env.GOOGLE_ANALYTICS_ID?.startsWith("UA-")) {
|
||||
|
||||
@@ -37,7 +37,11 @@ const DocumentInsights = lazyWithRetry(
|
||||
);
|
||||
const CommandBar = lazyWithRetry(() => import("~/components/CommandBar"));
|
||||
|
||||
const AuthenticatedLayout: React.FC = ({ children }) => {
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const AuthenticatedLayout: React.FC = ({ children }: Props) => {
|
||||
const { ui, auth } = useStores();
|
||||
const location = useLocation();
|
||||
const can = usePolicy(ui.activeCollectionId);
|
||||
|
||||
@@ -3,6 +3,7 @@ import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
withStickyHeader?: boolean;
|
||||
};
|
||||
|
||||
@@ -26,7 +27,7 @@ const Content = styled.div`
|
||||
`};
|
||||
`;
|
||||
|
||||
const CenteredContent: React.FC<Props> = ({ children, ...rest }) => (
|
||||
const CenteredContent: React.FC<Props> = ({ children, ...rest }: Props) => (
|
||||
<Container {...rest}>
|
||||
<Content>{children}</Content>
|
||||
</Container>
|
||||
|
||||
@@ -52,7 +52,11 @@ function CommandBar() {
|
||||
);
|
||||
}
|
||||
|
||||
const KBarPortal: React.FC = ({ children }) => {
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const KBarPortal: React.FC = ({ children }: Props) => {
|
||||
const { showing } = useKBar((state) => ({
|
||||
showing: state.visualState !== "hidden",
|
||||
}));
|
||||
|
||||
@@ -17,6 +17,7 @@ type Props = {
|
||||
danger?: boolean;
|
||||
/** Keep the submit button disabled */
|
||||
disabled?: boolean;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const ConfirmationDialog: React.FC<Props> = ({
|
||||
@@ -26,7 +27,7 @@ const ConfirmationDialog: React.FC<Props> = ({
|
||||
savingText,
|
||||
danger,
|
||||
disabled = false,
|
||||
}) => {
|
||||
}: Props) => {
|
||||
const [isSaving, setIsSaving] = React.useState(false);
|
||||
const { dialogs } = useStores();
|
||||
const { showToast } = useToasts();
|
||||
|
||||
@@ -30,144 +30,138 @@ export type RefHandle = {
|
||||
* Defines a content editable component with the same interface as a native
|
||||
* HTMLInputElement (or, as close as we can get).
|
||||
*/
|
||||
const ContentEditable = React.forwardRef(
|
||||
(
|
||||
{
|
||||
disabled,
|
||||
onChange,
|
||||
onInput,
|
||||
onBlur,
|
||||
onKeyDown,
|
||||
value,
|
||||
children,
|
||||
className,
|
||||
maxLength,
|
||||
autoFocus,
|
||||
placeholder,
|
||||
readOnly,
|
||||
dir,
|
||||
onClick,
|
||||
...rest
|
||||
}: Props,
|
||||
ref: React.RefObject<RefHandle>
|
||||
) => {
|
||||
const contentRef = React.useRef<HTMLSpanElement>(null);
|
||||
const [innerValue, setInnerValue] = React.useState<string>(value);
|
||||
const lastValue = React.useRef(value);
|
||||
const ContentEditable = React.forwardRef(function _ContentEditable(
|
||||
{
|
||||
disabled,
|
||||
onChange,
|
||||
onInput,
|
||||
onBlur,
|
||||
onKeyDown,
|
||||
value,
|
||||
children,
|
||||
className,
|
||||
maxLength,
|
||||
autoFocus,
|
||||
placeholder,
|
||||
readOnly,
|
||||
dir,
|
||||
onClick,
|
||||
...rest
|
||||
}: Props,
|
||||
ref: React.RefObject<RefHandle>
|
||||
) {
|
||||
const contentRef = React.useRef<HTMLSpanElement>(null);
|
||||
const [innerValue, setInnerValue] = React.useState<string>(value);
|
||||
const lastValue = React.useRef(value);
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.focus();
|
||||
// looks unnecessary but required because of https://github.com/outline/outline/issues/5198
|
||||
if (!contentRef.current.innerText) {
|
||||
placeCaret(contentRef.current, true);
|
||||
}
|
||||
}
|
||||
},
|
||||
focusAtStart: () => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.focus();
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.focus();
|
||||
// looks unnecessary but required because of https://github.com/outline/outline/issues/5198
|
||||
if (!contentRef.current.innerText) {
|
||||
placeCaret(contentRef.current, true);
|
||||
}
|
||||
},
|
||||
focusAtEnd: () => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.focus();
|
||||
placeCaret(contentRef.current, false);
|
||||
}
|
||||
},
|
||||
getComputedDirection: () => {
|
||||
if (contentRef.current) {
|
||||
return window.getComputedStyle(contentRef.current).direction;
|
||||
}
|
||||
return "ltr";
|
||||
},
|
||||
}));
|
||||
|
||||
const wrappedEvent =
|
||||
(
|
||||
callback:
|
||||
| React.FocusEventHandler<HTMLSpanElement>
|
||||
| React.FormEventHandler<HTMLSpanElement>
|
||||
| React.KeyboardEventHandler<HTMLSpanElement>
|
||||
| undefined
|
||||
) =>
|
||||
(event: any) => {
|
||||
if (readOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = event.currentTarget.textContent || "";
|
||||
|
||||
if (
|
||||
maxLength &&
|
||||
isPrintableKeyEvent(event) &&
|
||||
text.length >= maxLength
|
||||
) {
|
||||
event?.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (text !== lastValue.current) {
|
||||
lastValue.current = text;
|
||||
onChange?.(text);
|
||||
}
|
||||
|
||||
callback?.(event);
|
||||
};
|
||||
|
||||
// This is to account for being within a React.Suspense boundary, in this
|
||||
// case the component may be rendered with display: none. React 18 may solve
|
||||
// this in the future by delaying useEffect hooks:
|
||||
// https://github.com/facebook/react/issues/14536#issuecomment-861980492
|
||||
const isVisible = useOnScreen(contentRef);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (autoFocus && isVisible && !disabled && !readOnly) {
|
||||
contentRef.current?.focus();
|
||||
}
|
||||
}, [autoFocus, disabled, isVisible, readOnly, contentRef]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (contentRef.current && value !== contentRef.current.textContent) {
|
||||
setInnerValue(value);
|
||||
},
|
||||
focusAtStart: () => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.focus();
|
||||
placeCaret(contentRef.current, true);
|
||||
}
|
||||
}, [value, contentRef]);
|
||||
},
|
||||
focusAtEnd: () => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.focus();
|
||||
placeCaret(contentRef.current, false);
|
||||
}
|
||||
},
|
||||
getComputedDirection: () => {
|
||||
if (contentRef.current) {
|
||||
return window.getComputedStyle(contentRef.current).direction;
|
||||
}
|
||||
return "ltr";
|
||||
},
|
||||
}));
|
||||
|
||||
// Ensure only plain text can be pasted into input when pasting from another
|
||||
// rich text source. Note: If `onPaste` prop is passed then it takes
|
||||
// priority over this behavior.
|
||||
const handlePaste = React.useCallback(
|
||||
(event: React.ClipboardEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault();
|
||||
const text = event.clipboardData.getData("text/plain");
|
||||
window.document.execCommand("insertText", false, text);
|
||||
},
|
||||
[]
|
||||
);
|
||||
const wrappedEvent =
|
||||
(
|
||||
callback:
|
||||
| React.FocusEventHandler<HTMLSpanElement>
|
||||
| React.FormEventHandler<HTMLSpanElement>
|
||||
| React.KeyboardEventHandler<HTMLSpanElement>
|
||||
| undefined
|
||||
) =>
|
||||
(event: any) => {
|
||||
if (readOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className} dir={dir} onClick={onClick}>
|
||||
<Content
|
||||
ref={contentRef}
|
||||
contentEditable={!disabled && !readOnly}
|
||||
onInput={wrappedEvent(onInput)}
|
||||
onBlur={wrappedEvent(onBlur)}
|
||||
onKeyDown={wrappedEvent(onKeyDown)}
|
||||
onPaste={handlePaste}
|
||||
data-placeholder={placeholder}
|
||||
suppressContentEditableWarning
|
||||
role="textbox"
|
||||
{...rest}
|
||||
>
|
||||
{innerValue}
|
||||
</Content>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
const text = event.currentTarget.textContent || "";
|
||||
|
||||
if (maxLength && isPrintableKeyEvent(event) && text.length >= maxLength) {
|
||||
event?.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (text !== lastValue.current) {
|
||||
lastValue.current = text;
|
||||
onChange?.(text);
|
||||
}
|
||||
|
||||
callback?.(event);
|
||||
};
|
||||
|
||||
// This is to account for being within a React.Suspense boundary, in this
|
||||
// case the component may be rendered with display: none. React 18 may solve
|
||||
// this in the future by delaying useEffect hooks:
|
||||
// https://github.com/facebook/react/issues/14536#issuecomment-861980492
|
||||
const isVisible = useOnScreen(contentRef);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (autoFocus && isVisible && !disabled && !readOnly) {
|
||||
contentRef.current?.focus();
|
||||
}
|
||||
}, [autoFocus, disabled, isVisible, readOnly, contentRef]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (contentRef.current && value !== contentRef.current.textContent) {
|
||||
setInnerValue(value);
|
||||
}
|
||||
}, [value, contentRef]);
|
||||
|
||||
// Ensure only plain text can be pasted into input when pasting from another
|
||||
// rich text source. Note: If `onPaste` prop is passed then it takes
|
||||
// priority over this behavior.
|
||||
const handlePaste = React.useCallback(
|
||||
(event: React.ClipboardEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault();
|
||||
const text = event.clipboardData.getData("text/plain");
|
||||
window.document.execCommand("insertText", false, text);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={className} dir={dir} onClick={onClick}>
|
||||
<Content
|
||||
ref={contentRef}
|
||||
contentEditable={!disabled && !readOnly}
|
||||
onInput={wrappedEvent(onInput)}
|
||||
onBlur={wrappedEvent(onBlur)}
|
||||
onKeyDown={wrappedEvent(onKeyDown)}
|
||||
onPaste={handlePaste}
|
||||
data-placeholder={placeholder}
|
||||
suppressContentEditableWarning
|
||||
role="textbox"
|
||||
{...rest}
|
||||
>
|
||||
{innerValue}
|
||||
</Content>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
function placeCaret(element: HTMLElement, atStart: boolean) {
|
||||
if (
|
||||
|
||||
@@ -22,6 +22,7 @@ type Props = {
|
||||
level?: number;
|
||||
icon?: React.ReactElement;
|
||||
children?: React.ReactNode;
|
||||
ref?: React.LegacyRef<HTMLButtonElement> | undefined;
|
||||
};
|
||||
|
||||
const MenuItem = (
|
||||
@@ -80,7 +81,7 @@ const MenuItem = (
|
||||
</MenuAnchor>
|
||||
);
|
||||
},
|
||||
[active, as, hide, icon, onClick, ref, selected]
|
||||
[active, as, hide, icon, onClick, ref, children, selected]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -44,37 +44,35 @@ type SubMenuProps = MenuStateReturn & {
|
||||
title: React.ReactNode;
|
||||
};
|
||||
|
||||
const SubMenu = React.forwardRef(
|
||||
(
|
||||
{ templateItems, title, parentMenuState, ...rest }: SubMenuProps,
|
||||
ref: React.LegacyRef<HTMLButtonElement>
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const menu = useMenuState();
|
||||
const SubMenu = React.forwardRef(function _Template(
|
||||
{ templateItems, title, parentMenuState, ...rest }: SubMenuProps,
|
||||
ref: React.LegacyRef<HTMLButtonElement>
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const menu = useMenuState();
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuButton ref={ref} {...menu} {...rest}>
|
||||
{(props) => (
|
||||
<MenuAnchor disclosure {...props}>
|
||||
{title} <Disclosure color={theme.textTertiary} />
|
||||
</MenuAnchor>
|
||||
)}
|
||||
</MenuButton>
|
||||
<ContextMenu
|
||||
{...menu}
|
||||
aria-label={t("Submenu")}
|
||||
onClick={parentMenuState.hide}
|
||||
parentMenuState={parentMenuState}
|
||||
>
|
||||
<MouseSafeArea parentRef={menu.unstable_popoverRef} />
|
||||
<Template {...menu} items={templateItems} />
|
||||
</ContextMenu>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<MenuButton ref={ref} {...menu} {...rest}>
|
||||
{(props) => (
|
||||
<MenuAnchor disclosure {...props}>
|
||||
{title} <Disclosure color={theme.textTertiary} />
|
||||
</MenuAnchor>
|
||||
)}
|
||||
</MenuButton>
|
||||
<ContextMenu
|
||||
{...menu}
|
||||
aria-label={t("Submenu")}
|
||||
onClick={parentMenuState.hide}
|
||||
parentMenuState={parentMenuState}
|
||||
>
|
||||
<MouseSafeArea parentRef={menu.unstable_popoverRef} />
|
||||
<Template {...menu} items={templateItems} />
|
||||
</ContextMenu>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export function filterTemplateItems(items: TMenuItem[]): TMenuItem[] {
|
||||
return items
|
||||
|
||||
@@ -46,6 +46,7 @@ type Props = MenuStateReturn & {
|
||||
onClose?: () => void;
|
||||
/** Called when the context menu is clicked. */
|
||||
onClick?: (ev: React.MouseEvent) => void;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const ContextMenu: React.FC<Props> = ({
|
||||
@@ -54,7 +55,7 @@ const ContextMenu: React.FC<Props> = ({
|
||||
onClose,
|
||||
parentMenuState,
|
||||
...rest
|
||||
}) => {
|
||||
}: Props) => {
|
||||
const previousVisible = usePrevious(rest.visible);
|
||||
const maxHeight = useMenuHeight({
|
||||
visible: rest.visible,
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from "~/utils/routeHelpers";
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
document: Document;
|
||||
onlyText?: boolean;
|
||||
};
|
||||
@@ -58,7 +59,7 @@ const DocumentBreadcrumb: React.FC<Props> = ({
|
||||
document,
|
||||
children,
|
||||
onlyText,
|
||||
}) => {
|
||||
}: Props) => {
|
||||
const { collections } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const category = useCategory(document);
|
||||
@@ -129,7 +130,11 @@ const DocumentBreadcrumb: React.FC<Props> = ({
|
||||
);
|
||||
}
|
||||
|
||||
return <Breadcrumb items={items} children={children} highlightFirstItem />;
|
||||
return (
|
||||
<Breadcrumb items={items} highlightFirstItem>
|
||||
{children}
|
||||
</Breadcrumb>
|
||||
);
|
||||
};
|
||||
|
||||
const SmallSlash = styled(GoToIcon)`
|
||||
|
||||
@@ -335,16 +335,21 @@ function DocumentExplorer({ onSubmit, onSelect, items }: Props) {
|
||||
const innerElementType = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ style, ...rest }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
style={{
|
||||
...style,
|
||||
height: `${parseFloat(style?.height + "") + VERTICAL_PADDING * 2}px`,
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
));
|
||||
>(function innerElementType(
|
||||
{ style, ...rest }: React.HTMLAttributes<HTMLDivElement>,
|
||||
ref
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
style={{
|
||||
...style,
|
||||
height: `${parseFloat(style?.height + "") + VERTICAL_PADDING * 2}px`,
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Container tabIndex={-1} onKeyDown={handleKeyDown}>
|
||||
|
||||
@@ -15,6 +15,7 @@ import useCurrentUser from "~/hooks/useCurrentUser";
|
||||
import useStores from "~/hooks/useStores";
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
showCollection?: boolean;
|
||||
showPublished?: boolean;
|
||||
showLastViewed?: boolean;
|
||||
@@ -36,7 +37,7 @@ const DocumentMeta: React.FC<Props> = ({
|
||||
replace,
|
||||
to,
|
||||
...rest
|
||||
}) => {
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { collections } = useStores();
|
||||
const user = useCurrentUser();
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const EventBoundary: React.FC<Props> = ({ children, className }) => {
|
||||
const EventBoundary: React.FC<Props> = ({ children, className }: Props) => {
|
||||
const handleClick = React.useCallback((event: React.SyntheticEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
@@ -160,15 +160,16 @@ const EventListItem = ({ event, latest, document, ...rest }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const BaseItem = React.forwardRef(
|
||||
({ to, ...rest }: ItemProps, ref?: React.Ref<HTMLAnchorElement>) => {
|
||||
if (to) {
|
||||
return <CompositeListItem to={to} ref={ref} {...rest} />;
|
||||
}
|
||||
|
||||
return <ListItem ref={ref} {...rest} />;
|
||||
const BaseItem = React.forwardRef(function _BaseItem(
|
||||
{ to, ...rest }: ItemProps,
|
||||
ref?: React.Ref<HTMLAnchorElement>
|
||||
) {
|
||||
if (to) {
|
||||
return <CompositeListItem to={to} ref={ref} {...rest} />;
|
||||
}
|
||||
);
|
||||
|
||||
return <ListItem ref={ref} {...rest} />;
|
||||
});
|
||||
|
||||
const Subtitle = styled.span`
|
||||
svg {
|
||||
|
||||
@@ -99,7 +99,7 @@ function ExportDialog({ collection, onSubmit }: Props) {
|
||||
)}
|
||||
<Flex gap={12} column>
|
||||
{items.map((item) => (
|
||||
<Option>
|
||||
<Option key={item.value}>
|
||||
<input
|
||||
type="radio"
|
||||
name="format"
|
||||
|
||||
@@ -6,6 +6,7 @@ import Scrollable from "~/components/Scrollable";
|
||||
import usePrevious from "~/hooks/usePrevious";
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
isOpen: boolean;
|
||||
title?: string;
|
||||
onRequestClose: () => void;
|
||||
@@ -17,7 +18,7 @@ const Guide: React.FC<Props> = ({
|
||||
title = "Untitled",
|
||||
onRequestClose,
|
||||
...rest
|
||||
}) => {
|
||||
}: Props) => {
|
||||
const dialog = useDialogState({
|
||||
animated: 250,
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ export default function MarkdownIcon({
|
||||
<path
|
||||
d="M19.2692 7H3.86538C3.38745 7 3 7.38476 3 7.85938V16.2812C3 16.7559 3.38745 17.1406 3.86538 17.1406H19.2692C19.7472 17.1406 20.1346 16.7559 20.1346 16.2812V7.85938C20.1346 7.38476 19.7472 7 19.2692 7Z"
|
||||
stroke={color}
|
||||
stroke-width="2"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<path
|
||||
d="M5.16345 14.9922V9.14844H6.89422L8.62499 11.2969L10.3558 9.14844H12.0865V14.9922H10.3558V11.6406L8.62499 13.7891L6.89422 11.6406V14.9922H5.16345ZM15.9808 14.9922L13.3846 12.1562H15.1154V9.14844H16.8461V12.1562H18.5769L15.9808 14.9922Z"
|
||||
|
||||
@@ -16,7 +16,7 @@ type Props = Omit<InputProps, "onChange"> & {
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
|
||||
const InputColor: React.FC<Props> = ({ value, onChange, ...rest }) => {
|
||||
const InputColor: React.FC<Props> = ({ value, onChange, ...rest }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const menu = useMenuState({
|
||||
modal: true,
|
||||
|
||||
@@ -5,10 +5,11 @@ import { s } from "@shared/styles";
|
||||
import Flex from "~/components/Flex";
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
label: React.ReactNode | string;
|
||||
};
|
||||
|
||||
const Labeled: React.FC<Props> = ({ label, children, ...props }) => (
|
||||
const Labeled: React.FC<Props> = ({ label, children, ...props }: Props) => (
|
||||
<Flex column {...props}>
|
||||
<Label>{label}</Label>
|
||||
{children}
|
||||
|
||||
@@ -21,14 +21,14 @@ function Icon({ className }: { className?: string }) {
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M21 18H16L14 16V6C14 4.89543 14.8954 4 16 4H28C29.1046 4 30 4.89543 30 6V16C30 17.1046 29.1046 18 28 18H27L25.4142 19.5858C24.6332 20.3668 23.3668 20.3668 22.5858 19.5858L21 18ZM16 15.1716V6H28V16H27H26.1716L25.5858 16.5858L24 18.1716L22.4142 16.5858L21.8284 16H21H16.8284L16 15.1716Z"
|
||||
fill="#2B2F35"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M16 13H4C2.89543 13 2 13.8954 2 15V25C2 26.1046 2.89543 27 4 27H5L6.58579 28.5858C7.36684 29.3668 8.63316 29.3668 9.41421 28.5858L11 27H16C17.1046 27 18 26.1046 18 25V15C18 13.8954 17.1046 13 16 13ZM9 17L6 16.9681C6 16.9681 5 17.016 5 18C5 18.984 6 19 6 19H8.5H10C10 19 9.57627 20.1885 8.38983 21.0831C7.20339 21.9777 5.7197 23 5.7197 23C5.7197 23 4.99153 23.6054 5.5 24.5C6.00847 25.3946 7 24.8403 7 24.8403L9.74576 22.8722L11.9492 24.6614C11.9492 24.6614 12.6271 25.3771 13.3051 24.4825C13.9831 23.5879 13.3051 23.0512 13.3051 23.0512L11.1017 21.262C11.1017 21.262 11.5 21 12 20L12.5 19H14C14 19 15 19.0319 15 18C15 16.9681 14 16.9681 14 16.9681L11 17V16C11 16 11.0169 15 10 15C8.98305 15 9 16 9 16V17Z"
|
||||
fill="#2B2F35"
|
||||
/>
|
||||
|
||||
@@ -16,6 +16,7 @@ import useStores from "~/hooks/useStores";
|
||||
import { isModKey } from "~/utils/keyboard";
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
title?: string;
|
||||
sidebar?: React.ReactNode;
|
||||
sidebarRight?: React.ReactNode;
|
||||
@@ -26,7 +27,7 @@ const Layout: React.FC<Props> = ({
|
||||
children,
|
||||
sidebar,
|
||||
sidebarRight,
|
||||
}) => {
|
||||
}: Props) => {
|
||||
const { ui } = useStores();
|
||||
const sidebarCollapsed = !sidebar || ui.sidebarIsClosed;
|
||||
|
||||
|
||||
@@ -2,10 +2,14 @@ import * as React from "react";
|
||||
import Logger from "~/utils/Logger";
|
||||
import { loadPolyfills } from "~/utils/polyfills";
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Asyncronously load required polyfills. Should wrap the React tree.
|
||||
*/
|
||||
export const LazyPolyfill: React.FC = ({ children }) => {
|
||||
export const LazyPolyfill: React.FC = ({ children }: Props) => {
|
||||
const [isLoaded, setIsLoaded] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
@@ -21,6 +21,7 @@ function eachMinute(fn: () => void) {
|
||||
}
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
dateTime: string;
|
||||
tooltipDelay?: number;
|
||||
addSuffix?: boolean;
|
||||
@@ -37,7 +38,7 @@ const LocaleTime: React.FC<Props> = ({
|
||||
format,
|
||||
relative,
|
||||
tooltipDelay,
|
||||
}) => {
|
||||
}: Props) => {
|
||||
const userLocale: string = useUserLocale() || "";
|
||||
const dateFormatLong = {
|
||||
en_US: "MMMM do, yyyy h:mm a",
|
||||
|
||||
@@ -19,7 +19,9 @@ import Desktop from "~/utils/Desktop";
|
||||
import ErrorBoundary from "./ErrorBoundary";
|
||||
|
||||
let openModals = 0;
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
isOpen: boolean;
|
||||
isCentered?: boolean;
|
||||
title?: React.ReactNode;
|
||||
@@ -32,7 +34,7 @@ const Modal: React.FC<Props> = ({
|
||||
isCentered,
|
||||
title = "Untitled",
|
||||
onRequestClose,
|
||||
}) => {
|
||||
}: Props) => {
|
||||
const dialog = useDialogState({
|
||||
animated: 250,
|
||||
});
|
||||
|
||||
@@ -5,11 +5,12 @@ import Flex from "./Flex";
|
||||
import Text from "./Text";
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
icon?: JSX.Element;
|
||||
description?: JSX.Element;
|
||||
};
|
||||
|
||||
const Notice: React.FC<Props> = ({ children, icon, description }) => (
|
||||
const Notice: React.FC<Props> = ({ children, icon, description }: Props) => (
|
||||
<Container>
|
||||
<Flex as="span" gap={8}>
|
||||
{icon}
|
||||
|
||||
@@ -7,7 +7,11 @@ import { depths } from "@shared/styles";
|
||||
import Popover from "~/components/Popover";
|
||||
import Notifications from "./Notifications";
|
||||
|
||||
const NotificationsPopover: React.FC = ({ children }) => {
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const NotificationsPopover: React.FC = ({ children }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const scrollableRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ const Popover: React.FC<Props> = ({
|
||||
flex,
|
||||
mobilePosition,
|
||||
...rest
|
||||
}) => {
|
||||
}: Props) => {
|
||||
const isMobile = useMobile();
|
||||
|
||||
if (isMobile) {
|
||||
|
||||
@@ -11,6 +11,7 @@ type Props = {
|
||||
left?: React.ReactNode;
|
||||
actions?: React.ReactNode;
|
||||
centered?: boolean;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const Scene: React.FC<Props> = ({
|
||||
@@ -21,7 +22,7 @@ const Scene: React.FC<Props> = ({
|
||||
left,
|
||||
children,
|
||||
centered,
|
||||
}) => (
|
||||
}: Props) => (
|
||||
<FillWidth>
|
||||
<PageTitle title={textTitle || title} />
|
||||
<Header
|
||||
|
||||
@@ -17,7 +17,7 @@ import useStores from "~/hooks/useStores";
|
||||
import { SearchResult } from "~/types";
|
||||
import SearchListItem from "./SearchListItem";
|
||||
|
||||
type Props = { shareId: string };
|
||||
type Props = React.HTMLAttributes<HTMLInputElement> & { shareId: string };
|
||||
|
||||
function SearchPopover({ shareId }: Props) {
|
||||
const { t } = useTranslation();
|
||||
@@ -32,6 +32,7 @@ function SearchPopover({ shareId }: Props) {
|
||||
|
||||
const [query, setQuery] = React.useState("");
|
||||
const searchResults = documents.searchResults(query);
|
||||
const { show, hide } = popover;
|
||||
|
||||
const [cachedQuery, setCachedQuery] = React.useState(query);
|
||||
const [cachedSearchResults, setCachedSearchResults] = React.useState<
|
||||
@@ -42,9 +43,9 @@ function SearchPopover({ shareId }: Props) {
|
||||
if (searchResults) {
|
||||
setCachedQuery(query);
|
||||
setCachedSearchResults(searchResults);
|
||||
popover.show();
|
||||
show();
|
||||
}
|
||||
}, [searchResults, query, popover.show]);
|
||||
}, [searchResults, query, show]);
|
||||
|
||||
const performSearch = React.useCallback(
|
||||
async ({ query, ...options }) => {
|
||||
@@ -141,12 +142,12 @@ function SearchPopover({ shareId }: Props) {
|
||||
);
|
||||
|
||||
const handleSearchItemClick = React.useCallback(() => {
|
||||
popover.hide();
|
||||
hide();
|
||||
if (searchInputRef.current) {
|
||||
searchInputRef.current.value = "";
|
||||
focusRef.current = document.getElementById(bodyContentId);
|
||||
}
|
||||
}, [popover.hide]);
|
||||
}, [searchInputRef, hide]);
|
||||
|
||||
useKeyDown("/", (ev) => {
|
||||
if (
|
||||
|
||||
@@ -27,197 +27,198 @@ type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const Sidebar = React.forwardRef<HTMLDivElement, Props>(
|
||||
({ children }: Props, ref: React.RefObject<HTMLDivElement>) => {
|
||||
const [isCollapsing, setCollapsing] = React.useState(false);
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { ui, auth } = useStores();
|
||||
const location = useLocation();
|
||||
const previousLocation = usePrevious(location);
|
||||
const { isMenuOpen } = useMenuContext();
|
||||
const { user } = auth;
|
||||
const width = ui.sidebarWidth;
|
||||
const collapsed = ui.sidebarIsClosed && !isMenuOpen;
|
||||
const maxWidth = theme.sidebarMaxWidth;
|
||||
const minWidth = theme.sidebarMinWidth + 16; // padding
|
||||
const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
|
||||
{ children }: Props,
|
||||
ref: React.RefObject<HTMLDivElement>
|
||||
) {
|
||||
const [isCollapsing, setCollapsing] = React.useState(false);
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { ui, auth } = useStores();
|
||||
const location = useLocation();
|
||||
const previousLocation = usePrevious(location);
|
||||
const { isMenuOpen } = useMenuContext();
|
||||
const { user } = auth;
|
||||
const width = ui.sidebarWidth;
|
||||
const collapsed = ui.sidebarIsClosed && !isMenuOpen;
|
||||
const maxWidth = theme.sidebarMaxWidth;
|
||||
const minWidth = theme.sidebarMinWidth + 16; // padding
|
||||
|
||||
const setWidth = ui.setSidebarWidth;
|
||||
const [offset, setOffset] = React.useState(0);
|
||||
const [isAnimating, setAnimating] = React.useState(false);
|
||||
const [isResizing, setResizing] = React.useState(false);
|
||||
const isSmallerThanMinimum = width < minWidth;
|
||||
const setWidth = ui.setSidebarWidth;
|
||||
const [offset, setOffset] = React.useState(0);
|
||||
const [isAnimating, setAnimating] = React.useState(false);
|
||||
const [isResizing, setResizing] = React.useState(false);
|
||||
const isSmallerThanMinimum = width < minWidth;
|
||||
|
||||
const handleDrag = React.useCallback(
|
||||
(event: MouseEvent) => {
|
||||
// suppresses text selection
|
||||
event.preventDefault();
|
||||
// this is simple because the sidebar is always against the left edge
|
||||
const width = Math.min(event.pageX - offset, maxWidth);
|
||||
const isSmallerThanCollapsePoint = width < minWidth / 2;
|
||||
const handleDrag = React.useCallback(
|
||||
(event: MouseEvent) => {
|
||||
// suppresses text selection
|
||||
event.preventDefault();
|
||||
// this is simple because the sidebar is always against the left edge
|
||||
const width = Math.min(event.pageX - offset, maxWidth);
|
||||
const isSmallerThanCollapsePoint = width < minWidth / 2;
|
||||
|
||||
if (isSmallerThanCollapsePoint) {
|
||||
setWidth(theme.sidebarCollapsedWidth);
|
||||
} else {
|
||||
setWidth(width);
|
||||
}
|
||||
},
|
||||
[theme, offset, minWidth, maxWidth, setWidth]
|
||||
);
|
||||
|
||||
const handleStopDrag = React.useCallback(() => {
|
||||
setResizing(false);
|
||||
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
if (isSmallerThanMinimum) {
|
||||
const isSmallerThanCollapsePoint = width < minWidth / 2;
|
||||
|
||||
if (isSmallerThanCollapsePoint) {
|
||||
setAnimating(false);
|
||||
setCollapsing(true);
|
||||
ui.collapseSidebar();
|
||||
} else {
|
||||
setWidth(minWidth);
|
||||
setAnimating(true);
|
||||
}
|
||||
if (isSmallerThanCollapsePoint) {
|
||||
setWidth(theme.sidebarCollapsedWidth);
|
||||
} else {
|
||||
setWidth(width);
|
||||
}
|
||||
}, [ui, isSmallerThanMinimum, minWidth, width, setWidth]);
|
||||
},
|
||||
[theme, offset, minWidth, maxWidth, setWidth]
|
||||
);
|
||||
|
||||
const handleMouseDown = React.useCallback(
|
||||
(event) => {
|
||||
setOffset(event.pageX - width);
|
||||
setResizing(true);
|
||||
const handleStopDrag = React.useCallback(() => {
|
||||
setResizing(false);
|
||||
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
if (isSmallerThanMinimum) {
|
||||
const isSmallerThanCollapsePoint = width < minWidth / 2;
|
||||
|
||||
if (isSmallerThanCollapsePoint) {
|
||||
setAnimating(false);
|
||||
},
|
||||
[width]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isAnimating) {
|
||||
setTimeout(() => setAnimating(false), ANIMATION_MS);
|
||||
setCollapsing(true);
|
||||
ui.collapseSidebar();
|
||||
} else {
|
||||
setWidth(minWidth);
|
||||
setAnimating(true);
|
||||
}
|
||||
}, [isAnimating]);
|
||||
} else {
|
||||
setWidth(width);
|
||||
}
|
||||
}, [ui, isSmallerThanMinimum, minWidth, width, setWidth]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isCollapsing) {
|
||||
setTimeout(() => {
|
||||
setWidth(minWidth);
|
||||
setCollapsing(false);
|
||||
}, ANIMATION_MS);
|
||||
}
|
||||
}, [setWidth, minWidth, isCollapsing]);
|
||||
const handleMouseDown = React.useCallback(
|
||||
(event) => {
|
||||
setOffset(event.pageX - width);
|
||||
setResizing(true);
|
||||
setAnimating(false);
|
||||
},
|
||||
[width]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isResizing) {
|
||||
document.addEventListener("mousemove", handleDrag);
|
||||
document.addEventListener("mouseup", handleStopDrag);
|
||||
}
|
||||
React.useEffect(() => {
|
||||
if (isAnimating) {
|
||||
setTimeout(() => setAnimating(false), ANIMATION_MS);
|
||||
}
|
||||
}, [isAnimating]);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", handleDrag);
|
||||
document.removeEventListener("mouseup", handleStopDrag);
|
||||
};
|
||||
}, [isResizing, handleDrag, handleStopDrag]);
|
||||
React.useEffect(() => {
|
||||
if (isCollapsing) {
|
||||
setTimeout(() => {
|
||||
setWidth(minWidth);
|
||||
setCollapsing(false);
|
||||
}, ANIMATION_MS);
|
||||
}
|
||||
}, [setWidth, minWidth, isCollapsing]);
|
||||
|
||||
const handleReset = React.useCallback(() => {
|
||||
ui.setSidebarWidth(theme.sidebarWidth);
|
||||
}, [ui, theme.sidebarWidth]);
|
||||
React.useEffect(() => {
|
||||
if (isResizing) {
|
||||
document.addEventListener("mousemove", handleDrag);
|
||||
document.addEventListener("mouseup", handleStopDrag);
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
ui.setSidebarResizing(isResizing);
|
||||
}, [ui, isResizing]);
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", handleDrag);
|
||||
document.removeEventListener("mouseup", handleStopDrag);
|
||||
};
|
||||
}, [isResizing, handleDrag, handleStopDrag]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (location !== previousLocation) {
|
||||
ui.hideMobileSidebar();
|
||||
}
|
||||
}, [ui, location, previousLocation]);
|
||||
const handleReset = React.useCallback(() => {
|
||||
ui.setSidebarWidth(theme.sidebarWidth);
|
||||
}, [ui, theme.sidebarWidth]);
|
||||
|
||||
const style = React.useMemo(
|
||||
() => ({
|
||||
width: `${width}px`,
|
||||
}),
|
||||
[width]
|
||||
);
|
||||
React.useEffect(() => {
|
||||
ui.setSidebarResizing(isResizing);
|
||||
}, [ui, isResizing]);
|
||||
|
||||
const toggleStyle = React.useMemo(
|
||||
() => ({
|
||||
right: "auto",
|
||||
marginLeft: `${collapsed ? theme.sidebarCollapsedWidth : width}px`,
|
||||
}),
|
||||
[width, theme.sidebarCollapsedWidth, collapsed]
|
||||
);
|
||||
React.useEffect(() => {
|
||||
if (location !== previousLocation) {
|
||||
ui.hideMobileSidebar();
|
||||
}
|
||||
}, [ui, location, previousLocation]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container
|
||||
ref={ref}
|
||||
style={style}
|
||||
$isAnimating={isAnimating}
|
||||
$isSmallerThanMinimum={isSmallerThanMinimum}
|
||||
$mobileSidebarVisible={ui.mobileSidebarVisible}
|
||||
$collapsed={collapsed}
|
||||
column
|
||||
>
|
||||
{ui.mobileSidebarVisible && (
|
||||
<Portal>
|
||||
<Backdrop onClick={ui.toggleMobileSidebar} />
|
||||
</Portal>
|
||||
)}
|
||||
{children}
|
||||
const style = React.useMemo(
|
||||
() => ({
|
||||
width: `${width}px`,
|
||||
}),
|
||||
[width]
|
||||
);
|
||||
|
||||
{user && (
|
||||
<AccountMenu>
|
||||
{(props: HeaderButtonProps) => (
|
||||
<HeaderButton
|
||||
{...props}
|
||||
showMoreMenu
|
||||
title={user.name}
|
||||
image={
|
||||
<StyledAvatar
|
||||
alt={user.name}
|
||||
model={user}
|
||||
size={24}
|
||||
showBorder={false}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<NotificationsPopover>
|
||||
{(rest: HeaderButtonProps) => (
|
||||
<HeaderButton {...rest} image={<NotificationIcon />} />
|
||||
)}
|
||||
</NotificationsPopover>
|
||||
</HeaderButton>
|
||||
)}
|
||||
</AccountMenu>
|
||||
)}
|
||||
<ResizeBorder
|
||||
onMouseDown={handleMouseDown}
|
||||
onDoubleClick={ui.sidebarIsClosed ? undefined : handleReset}
|
||||
/>
|
||||
{ui.sidebarIsClosed && (
|
||||
<Toggle
|
||||
onClick={ui.toggleCollapsedSidebar}
|
||||
direction={"right"}
|
||||
aria-label={t("Expand")}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
<Toggle
|
||||
style={toggleStyle}
|
||||
onClick={ui.toggleCollapsedSidebar}
|
||||
direction={ui.sidebarIsClosed ? "right" : "left"}
|
||||
aria-label={ui.sidebarIsClosed ? t("Expand") : t("Collapse")}
|
||||
const toggleStyle = React.useMemo(
|
||||
() => ({
|
||||
right: "auto",
|
||||
marginLeft: `${collapsed ? theme.sidebarCollapsedWidth : width}px`,
|
||||
}),
|
||||
[width, theme.sidebarCollapsedWidth, collapsed]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container
|
||||
ref={ref}
|
||||
style={style}
|
||||
$isAnimating={isAnimating}
|
||||
$isSmallerThanMinimum={isSmallerThanMinimum}
|
||||
$mobileSidebarVisible={ui.mobileSidebarVisible}
|
||||
$collapsed={collapsed}
|
||||
column
|
||||
>
|
||||
{ui.mobileSidebarVisible && (
|
||||
<Portal>
|
||||
<Backdrop onClick={ui.toggleMobileSidebar} />
|
||||
</Portal>
|
||||
)}
|
||||
{children}
|
||||
|
||||
{user && (
|
||||
<AccountMenu>
|
||||
{(props: HeaderButtonProps) => (
|
||||
<HeaderButton
|
||||
{...props}
|
||||
showMoreMenu
|
||||
title={user.name}
|
||||
image={
|
||||
<StyledAvatar
|
||||
alt={user.name}
|
||||
model={user}
|
||||
size={24}
|
||||
showBorder={false}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<NotificationsPopover>
|
||||
{(rest: HeaderButtonProps) => (
|
||||
<HeaderButton {...rest} image={<NotificationIcon />} />
|
||||
)}
|
||||
</NotificationsPopover>
|
||||
</HeaderButton>
|
||||
)}
|
||||
</AccountMenu>
|
||||
)}
|
||||
<ResizeBorder
|
||||
onMouseDown={handleMouseDown}
|
||||
onDoubleClick={ui.sidebarIsClosed ? undefined : handleReset}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
{ui.sidebarIsClosed && (
|
||||
<Toggle
|
||||
onClick={ui.toggleCollapsedSidebar}
|
||||
direction={"right"}
|
||||
aria-label={t("Expand")}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
<Toggle
|
||||
style={toggleStyle}
|
||||
onClick={ui.toggleCollapsedSidebar}
|
||||
direction={ui.sidebarIsClosed ? "right" : "left"}
|
||||
aria-label={ui.sidebarIsClosed ? t("Expand") : t("Collapse")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const StyledAvatar = styled(Avatar)`
|
||||
margin-left: 4px;
|
||||
|
||||
@@ -37,7 +37,7 @@ const CollectionLink: React.FC<Props> = ({
|
||||
expanded,
|
||||
onDisclosureClick,
|
||||
isDraggingAnyCollection,
|
||||
}) => {
|
||||
}: Props) => {
|
||||
const itemRef = React.useRef<
|
||||
NavigationNode & { depth: number; active: boolean; collectionId: string }
|
||||
>();
|
||||
|
||||
@@ -3,9 +3,10 @@ import styled from "styled-components";
|
||||
|
||||
type Props = {
|
||||
expanded: boolean;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const Folder: React.FC<Props> = ({ expanded, children }) => {
|
||||
const Folder: React.FC<Props> = ({ expanded, children }: Props) => {
|
||||
const [openedOnce, setOpenedOnce] = React.useState(expanded);
|
||||
|
||||
// allows us to avoid rendering all children when the folder hasn't been opened
|
||||
|
||||
@@ -9,12 +9,13 @@ type Props = {
|
||||
/** Unique header id – if passed the header will become toggleable */
|
||||
id?: string;
|
||||
title: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggleable sidebar header
|
||||
*/
|
||||
export const Header: React.FC<Props> = ({ id, title, children }) => {
|
||||
export const Header: React.FC<Props> = ({ id, title, children }: Props) => {
|
||||
const [firstRender, setFirstRender] = React.useState(true);
|
||||
const [expanded, setExpanded] = usePersistedState<boolean>(
|
||||
`sidebar-header-${id}`,
|
||||
|
||||
@@ -17,7 +17,7 @@ export type HeaderButtonProps = React.ComponentProps<typeof Button> & {
|
||||
};
|
||||
|
||||
const HeaderButton = React.forwardRef<HTMLButtonElement, HeaderButtonProps>(
|
||||
(
|
||||
function _HeaderButton(
|
||||
{
|
||||
showDisclosure,
|
||||
showMoreMenu,
|
||||
@@ -28,25 +28,27 @@ const HeaderButton = React.forwardRef<HTMLButtonElement, HeaderButtonProps>(
|
||||
...rest
|
||||
}: HeaderButtonProps,
|
||||
ref
|
||||
) => (
|
||||
<Flex justify="space-between" align="center" shrink={false}>
|
||||
<Button
|
||||
{...rest}
|
||||
minHeight={minHeight}
|
||||
as="button"
|
||||
ref={ref}
|
||||
role="button"
|
||||
>
|
||||
<Title gap={8} align="center">
|
||||
{image}
|
||||
{title}
|
||||
</Title>
|
||||
{showDisclosure && <ExpandedIcon />}
|
||||
{showMoreMenu && <MoreIcon />}
|
||||
</Button>
|
||||
{children}
|
||||
</Flex>
|
||||
)
|
||||
) {
|
||||
return (
|
||||
<Flex justify="space-between" align="center" shrink={false}>
|
||||
<Button
|
||||
{...rest}
|
||||
minHeight={minHeight}
|
||||
as="button"
|
||||
ref={ref}
|
||||
role="button"
|
||||
>
|
||||
<Title gap={8} align="center">
|
||||
{image}
|
||||
{title}
|
||||
</Title>
|
||||
{showDisclosure && <ExpandedIcon />}
|
||||
{showMoreMenu && <MoreIcon />}
|
||||
</Button>
|
||||
{children}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const Title = styled(Flex)`
|
||||
|
||||
@@ -12,41 +12,42 @@ type Props = {
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
};
|
||||
|
||||
const Toggle = React.forwardRef<HTMLButtonElement, Props>(
|
||||
({ direction = "left", onClick, style }: Props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [hovering, setHovering] = React.useState(false);
|
||||
const positionRef = React.useRef<HTMLDivElement>(null);
|
||||
const Toggle = React.forwardRef<HTMLButtonElement, Props>(function Toggle_(
|
||||
{ direction = "left", onClick, style }: Props,
|
||||
ref
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
const [hovering, setHovering] = React.useState(false);
|
||||
const positionRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
// Not using CSS hover here so that we can disable pointer events on this
|
||||
// div and allow click through to the editor elements behind.
|
||||
useEventListener("mousemove", (event: MouseEvent) => {
|
||||
if (!positionRef.current) {
|
||||
return;
|
||||
}
|
||||
// Not using CSS hover here so that we can disable pointer events on this
|
||||
// div and allow click through to the editor elements behind.
|
||||
useEventListener("mousemove", (event: MouseEvent) => {
|
||||
if (!positionRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bound = positionRef.current.getBoundingClientRect();
|
||||
const withinBounds =
|
||||
event.clientX >= bound.left && event.clientX <= bound.right;
|
||||
if (withinBounds !== hovering) {
|
||||
setHovering(withinBounds);
|
||||
}
|
||||
});
|
||||
const bound = positionRef.current.getBoundingClientRect();
|
||||
const withinBounds =
|
||||
event.clientX >= bound.left && event.clientX <= bound.right;
|
||||
if (withinBounds !== hovering) {
|
||||
setHovering(withinBounds);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Positioner style={style} ref={positionRef} $hovering={hovering}>
|
||||
<ToggleButton
|
||||
ref={ref}
|
||||
$direction={direction}
|
||||
onClick={onClick}
|
||||
aria-label={t("Toggle sidebar")}
|
||||
>
|
||||
<Arrow />
|
||||
</ToggleButton>
|
||||
</Positioner>
|
||||
);
|
||||
}
|
||||
);
|
||||
return (
|
||||
<Positioner style={style} ref={positionRef} $hovering={hovering}>
|
||||
<ToggleButton
|
||||
ref={ref}
|
||||
$direction={direction}
|
||||
onClick={onClick}
|
||||
aria-label={t("Toggle sidebar")}
|
||||
>
|
||||
<Arrow />
|
||||
</ToggleButton>
|
||||
</Positioner>
|
||||
);
|
||||
});
|
||||
|
||||
export const ToggleButton = styled.button<{ $direction?: "left" | "right" }>`
|
||||
opacity: 0;
|
||||
|
||||
@@ -5,9 +5,10 @@ import Flex from "./Flex";
|
||||
type Props = {
|
||||
size?: number;
|
||||
color?: string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const Squircle: React.FC<Props> = ({ color, size = 28, children }) => (
|
||||
const Squircle: React.FC<Props> = ({ color, size = 28, children }: Props) => (
|
||||
<Wrapper
|
||||
style={{ width: size, height: size }}
|
||||
align="center"
|
||||
|
||||
@@ -3,6 +3,7 @@ import styled from "styled-components";
|
||||
import { s } from "@shared/styles";
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
sticky?: boolean;
|
||||
};
|
||||
|
||||
@@ -34,7 +35,7 @@ const Background = styled.div<{ sticky?: boolean }>`
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
const Subheading: React.FC<Props> = ({ children, sticky, ...rest }) => (
|
||||
const Subheading: React.FC<Props> = ({ children, sticky, ...rest }: Props) => (
|
||||
<Background sticky={sticky}>
|
||||
<H3 {...rest}>
|
||||
<Underline>{children}</Underline>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { hover } from "~/styles";
|
||||
type Props = Omit<React.ComponentProps<typeof NavLink>, "children"> & {
|
||||
to: string;
|
||||
exact?: boolean;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const TabLink = styled(NavLink)`
|
||||
@@ -44,7 +45,7 @@ const transition = {
|
||||
damping: 30,
|
||||
};
|
||||
|
||||
const Tab: React.FC<Props> = ({ children, ...rest }) => {
|
||||
const Tab: React.FC<Props> = ({ children, ...rest }: Props) => {
|
||||
const theme = useTheme();
|
||||
const activeStyle = {
|
||||
color: theme.textSecondary,
|
||||
|
||||
@@ -121,9 +121,12 @@ function Table({
|
||||
<InnerTable {...getTableProps()}>
|
||||
<thead>
|
||||
{headerGroups.map((headerGroup) => (
|
||||
<tr {...headerGroup.getHeaderGroupProps()}>
|
||||
<tr {...headerGroup.getHeaderGroupProps()} key={headerGroup.id}>
|
||||
{headerGroup.headers.map((column) => (
|
||||
<Head {...column.getHeaderProps(column.getSortByToggleProps())}>
|
||||
<Head
|
||||
{...column.getHeaderProps(column.getSortByToggleProps())}
|
||||
key={column.id}
|
||||
>
|
||||
<SortWrapper
|
||||
align="center"
|
||||
$sortable={!column.disableSortBy}
|
||||
@@ -146,7 +149,7 @@ function Table({
|
||||
{rows.map((row) => {
|
||||
prepareRow(row);
|
||||
return (
|
||||
<Row {...row.getRowProps()}>
|
||||
<Row {...row.getRowProps()} key={row.id}>
|
||||
{row.cells.map((cell) => (
|
||||
<Cell
|
||||
{...cell.getCellProps([
|
||||
@@ -155,6 +158,7 @@ function Table({
|
||||
className: cell.column.className,
|
||||
},
|
||||
])}
|
||||
key={cell.column.id}
|
||||
>
|
||||
{cell.render("Cell")}
|
||||
</Cell>
|
||||
|
||||
@@ -57,7 +57,11 @@ export const Separator = styled.span`
|
||||
margin-top: 6px;
|
||||
`;
|
||||
|
||||
const Tabs: React.FC = ({ children }) => {
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const Tabs: React.FC = ({ children }: Props) => {
|
||||
const ref = React.useRef<any>();
|
||||
const [shadowVisible, setShadow] = React.useState(false);
|
||||
const { width } = useWindowSize();
|
||||
|
||||
@@ -7,7 +7,11 @@ import useBuildTheme from "~/hooks/useBuildTheme";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { TooltipStyles } from "./Tooltip";
|
||||
|
||||
const Theme: React.FC = ({ children }) => {
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const Theme: React.FC = ({ children }: Props) => {
|
||||
const { auth, ui } = useStores();
|
||||
const theme = useBuildTheme(
|
||||
auth.team?.getPreference(TeamPreference.CustomTheme) ||
|
||||
|
||||
@@ -3,10 +3,11 @@ import styled from "styled-components";
|
||||
import Tooltip from "~/components/Tooltip";
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
tooltip?: string;
|
||||
};
|
||||
|
||||
const WrappedTooltip: React.FC<Props> = ({ children, tooltip }) => (
|
||||
const WrappedTooltip: React.FC<Props> = ({ children, tooltip }: Props) => (
|
||||
<Tooltip offset={[0, 16]} delay={150} tooltip={tooltip} placement="top">
|
||||
<TooltipContent>{children}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -863,11 +863,13 @@ const EditorContainer = styled(Styles)<{ focusedCommentId?: string }>`
|
||||
`;
|
||||
|
||||
const LazyLoadedEditor = React.forwardRef<Editor, Props>(
|
||||
(props: Props, ref) => (
|
||||
<WithTheme>
|
||||
{(theme) => <Editor theme={theme} {...props} ref={ref} />}
|
||||
</WithTheme>
|
||||
)
|
||||
function _LazyLoadedEditor(props: Props, ref) {
|
||||
return (
|
||||
<WithTheme>
|
||||
{(theme) => <Editor theme={theme} {...props} ref={ref} />}
|
||||
</WithTheme>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const observe = (
|
||||
|
||||
@@ -8,7 +8,11 @@ type MenuContextType = {
|
||||
|
||||
const MenuContext = React.createContext<MenuContextType | null>(null);
|
||||
|
||||
export const MenuProvider: React.FC = ({ children }) => {
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const MenuProvider: React.FC = ({ children }: Props) => {
|
||||
const [isMenuOpen, setIsMenuOpen] = React.useState(false);
|
||||
const memoized = React.useMemo(
|
||||
() => ({
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import * as React from "react";
|
||||
|
||||
const isSupported = "IntersectionObserver" in window;
|
||||
|
||||
/**
|
||||
* Hook to return if a given ref is visible on screen.
|
||||
*
|
||||
* @returns boolean if the node is visible
|
||||
*/
|
||||
export default function useOnScreen(ref: React.RefObject<HTMLElement>) {
|
||||
const isSupported = "IntersectionObserver" in window;
|
||||
const [isIntersecting, setIntersecting] = React.useState(!isSupported);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -28,7 +29,7 @@ export default function useOnScreen(ref: React.RefObject<HTMLElement>) {
|
||||
observer?.unobserve(element);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}, [ref]);
|
||||
|
||||
return isIntersecting;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,11 @@ import usePrevious from "~/hooks/usePrevious";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import separator from "~/menus/separator";
|
||||
|
||||
const AccountMenu: React.FC = ({ children }) => {
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const AccountMenu: React.FC = ({ children }: Props) => {
|
||||
const menu = useMenuState({
|
||||
placement: "bottom-end",
|
||||
modal: true,
|
||||
|
||||
@@ -11,7 +11,11 @@ import usePrevious from "~/hooks/usePrevious";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import separator from "~/menus/separator";
|
||||
|
||||
const OrganizationMenu: React.FC = ({ children }) => {
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const OrganizationMenu: React.FC = ({ children }: Props) => {
|
||||
const menu = useMenuState({
|
||||
unstable_offset: [4, -4],
|
||||
placement: "bottom-start",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from "react";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Button from "~/components/Button";
|
||||
import Flex from "~/components/Flex";
|
||||
import Input from "~/components/Input";
|
||||
@@ -51,11 +51,9 @@ function APITokenNew({ onSubmit }: Props) {
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Text type="secondary">
|
||||
<Trans>
|
||||
Name your token something that will help you to remember it's use in
|
||||
the future, for example "local development", "production", or
|
||||
"continuous integration".
|
||||
</Trans>
|
||||
{t(
|
||||
`Name your token something that will help you to remember it's use in the future, for example "local development", "production", or "continuous integration".`
|
||||
)}
|
||||
</Text>
|
||||
<Flex>
|
||||
<Input
|
||||
|
||||
@@ -12,6 +12,7 @@ type Props = {
|
||||
disabled: boolean;
|
||||
accept: string;
|
||||
collectionId: string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const DropToImport: React.FC<Props> = ({
|
||||
@@ -19,7 +20,7 @@ const DropToImport: React.FC<Props> = ({
|
||||
disabled,
|
||||
accept,
|
||||
collectionId,
|
||||
}) => {
|
||||
}: Props) => {
|
||||
const { handleFiles, isImporting } = useImportDocument(collectionId);
|
||||
const { showToast } = useToasts();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -5,6 +5,7 @@ import { MenuInternalLink } from "~/types";
|
||||
import { sharedDocumentPath } from "~/utils/routeHelpers";
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
documentId: string;
|
||||
shareId: string;
|
||||
sharedTree: NavigationNode | undefined;
|
||||
@@ -44,7 +45,7 @@ const PublicBreadcrumb: React.FC<Props> = ({
|
||||
shareId,
|
||||
sharedTree,
|
||||
children,
|
||||
}) => {
|
||||
}: Props) => {
|
||||
const items: MenuInternalLink[] = React.useMemo(
|
||||
() =>
|
||||
pathToDocument(sharedTree, documentId)
|
||||
@@ -57,7 +58,7 @@ const PublicBreadcrumb: React.FC<Props> = ({
|
||||
[sharedTree, shareId, documentId]
|
||||
);
|
||||
|
||||
return <Breadcrumb items={items} children={children} />;
|
||||
return <Breadcrumb items={items}>{children}</Breadcrumb>;
|
||||
};
|
||||
|
||||
export default PublicBreadcrumb;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/no-unescaped-entities */
|
||||
import { WarningIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
@@ -50,7 +50,11 @@ function ApiKeys() {
|
||||
For more details see the <em>developer documentation</em>."
|
||||
components={{
|
||||
em: (
|
||||
<a href="https://www.getoutline.com/developers" target="_blank" />
|
||||
<a
|
||||
href="https://www.getoutline.com/developers"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -10,10 +10,11 @@ import Button from "~/components/Button";
|
||||
import Text from "~/components/Text";
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
title: React.ReactNode;
|
||||
};
|
||||
|
||||
const HelpDisclosure: React.FC<Props> = ({ title, children }) => {
|
||||
const HelpDisclosure: React.FC<Props> = ({ title, children }: Props) => {
|
||||
const disclosure = useDisclosureState({ animated: true });
|
||||
const theme = useTheme();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { FileOperationFormat } from "@shared/types";
|
||||
import Flex from "~/components/Flex";
|
||||
import Text from "~/components/Text";
|
||||
@@ -8,6 +8,7 @@ import DropToImport from "./DropToImport";
|
||||
import HelpDisclosure from "./HelpDisclosure";
|
||||
|
||||
function ImportNotionDialog() {
|
||||
const { t } = useTranslation();
|
||||
const { dialogs } = useStores();
|
||||
|
||||
return (
|
||||
@@ -17,10 +18,11 @@ function ImportNotionDialog() {
|
||||
onSubmit={dialogs.closeAllModals}
|
||||
format={FileOperationFormat.Notion}
|
||||
>
|
||||
<Trans>
|
||||
Drag and drop the zip file from Notion's HTML export option, or
|
||||
click to upload
|
||||
</Trans>
|
||||
<>
|
||||
{t(
|
||||
`Drag and drop the zip file from Notion's HTML export option, or click to upload`
|
||||
)}
|
||||
</>
|
||||
</DropToImport>
|
||||
</Text>
|
||||
<HelpDisclosure title={<Trans>Where do I find the file?</Trans>}>
|
||||
|
||||
@@ -6,6 +6,7 @@ import Flex from "~/components/Flex";
|
||||
import Text from "~/components/Text";
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
label: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
name: string;
|
||||
@@ -63,7 +64,7 @@ const SettingRow: React.FC<Props> = ({
|
||||
label,
|
||||
border,
|
||||
children,
|
||||
}) => {
|
||||
}: Props) => {
|
||||
if (visible === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user