chore: Move to Typescript (#2783)
This PR moves the entire project to Typescript. Due to the ~1000 ignores this will lead to a messy codebase for a while, but the churn is worth it – all of those ignore comments are places that were never type-safe previously. closes #1282
This commit is contained in:
177
app/components/ContextMenu/index.tsx
Normal file
177
app/components/ContextMenu/index.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import * as React from "react";
|
||||
import { Portal } from "react-portal";
|
||||
import { Menu } from "reakit/Menu";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import useMenuHeight from "~/hooks/useMenuHeight";
|
||||
import usePrevious from "~/hooks/usePrevious";
|
||||
import {
|
||||
fadeIn,
|
||||
fadeAndSlideUp,
|
||||
fadeAndSlideDown,
|
||||
mobileContextMenu,
|
||||
} from "~/styles/animations";
|
||||
|
||||
export type Placement =
|
||||
| "auto-start"
|
||||
| "auto"
|
||||
| "auto-end"
|
||||
| "top-start"
|
||||
| "top"
|
||||
| "top-end"
|
||||
| "right-start"
|
||||
| "right"
|
||||
| "right-end"
|
||||
| "bottom-end"
|
||||
| "bottom"
|
||||
| "bottom-start"
|
||||
| "left-end"
|
||||
| "left"
|
||||
| "left-start";
|
||||
|
||||
type Props = {
|
||||
"aria-label": string;
|
||||
visible?: boolean;
|
||||
placement?: Placement;
|
||||
animating?: boolean;
|
||||
children: React.ReactNode;
|
||||
unstable_disclosureRef?: React.RefObject<HTMLElement | null>;
|
||||
onOpen?: () => void;
|
||||
onClose?: () => void;
|
||||
hide?: () => void;
|
||||
};
|
||||
|
||||
export default function ContextMenu({
|
||||
children,
|
||||
onOpen,
|
||||
onClose,
|
||||
...rest
|
||||
}: Props) {
|
||||
const previousVisible = usePrevious(rest.visible);
|
||||
const maxHeight = useMenuHeight(rest.visible, rest.unstable_disclosureRef);
|
||||
const backgroundRef = React.useRef();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (rest.visible && !previousVisible) {
|
||||
if (onOpen) {
|
||||
onOpen();
|
||||
}
|
||||
}
|
||||
|
||||
if (!rest.visible && previousVisible) {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
}, [onOpen, onClose, previousVisible, rest.visible]);
|
||||
|
||||
// Perf win – don't render anything until the menu has been opened
|
||||
if (!rest.visible && !previousVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// sets the menu height based on the available space between the disclosure/
|
||||
// trigger and the bottom of the window
|
||||
return (
|
||||
<>
|
||||
<Menu hideOnClickOutside preventBodyScroll {...rest}>
|
||||
{(props) => {
|
||||
// kind of hacky, but this is an effective way of telling which way
|
||||
// the menu will _actually_ be placed when taking into account screen
|
||||
// positioning.
|
||||
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
|
||||
const topAnchor = props.style.top === "0";
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'placement' does not exist on type 'Extra... Remove this comment to see the full error message
|
||||
const rightAnchor = props.placement === "bottom-end";
|
||||
|
||||
return (
|
||||
<Position {...props}>
|
||||
<Background
|
||||
dir="auto"
|
||||
topAnchor={topAnchor}
|
||||
rightAnchor={rightAnchor}
|
||||
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
|
||||
ref={backgroundRef}
|
||||
style={
|
||||
maxHeight && topAnchor
|
||||
? {
|
||||
maxHeight,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{rest.visible || rest.animating ? children : null}
|
||||
</Background>
|
||||
</Position>
|
||||
);
|
||||
}}
|
||||
</Menu>
|
||||
{(rest.visible || rest.animating) && (
|
||||
<Portal>
|
||||
<Backdrop onClick={rest.hide} />
|
||||
</Portal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const Backdrop = styled.div`
|
||||
animation: ${fadeIn} 200ms ease-in-out;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: ${(props) => props.theme.backdrop};
|
||||
z-index: ${(props) => props.theme.depths.menu - 1};
|
||||
|
||||
${breakpoint("tablet")`
|
||||
display: none;
|
||||
`};
|
||||
`;
|
||||
|
||||
export const Position = styled.div`
|
||||
position: absolute;
|
||||
z-index: ${(props) => props.theme.depths.menu};
|
||||
|
||||
${breakpoint("mobile", "tablet")`
|
||||
position: fixed !important;
|
||||
transform: none !important;
|
||||
top: auto !important;
|
||||
right: 8px !important;
|
||||
bottom: 16px !important;
|
||||
left: 8px !important;
|
||||
`};
|
||||
`;
|
||||
|
||||
export const Background = styled.div<{
|
||||
topAnchor?: boolean;
|
||||
rightAnchor?: boolean;
|
||||
}>`
|
||||
animation: ${mobileContextMenu} 200ms ease;
|
||||
transform-origin: 50% 100%;
|
||||
max-width: 100%;
|
||||
background: ${(props) => props.theme.menuBackground};
|
||||
border-radius: 6px;
|
||||
padding: 6px 0;
|
||||
min-width: 180px;
|
||||
min-height: 44px;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
max-height: 75vh;
|
||||
pointer-events: all;
|
||||
font-weight: normal;
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
|
||||
${breakpoint("tablet")`
|
||||
animation: ${(props: any) =>
|
||||
props.topAnchor ? fadeAndSlideDown : fadeAndSlideUp} 200ms ease;
|
||||
transform-origin: ${(props: any) => (props.rightAnchor ? "75%" : "25%")} 0;
|
||||
max-width: 276px;
|
||||
background: ${(props: any) => props.theme.menuBackground};
|
||||
box-shadow: ${(props: any) => props.theme.menuShadow};
|
||||
`};
|
||||
`;
|
||||
Reference in New Issue
Block a user