Move sidebar toggle into the sidebar itself instead of overlaying document content (#5749)
This commit is contained in:
@@ -1,64 +1,62 @@
|
||||
import { ExpandedIcon, MoreIcon } from "outline-icons";
|
||||
import { MoreIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { s } from "@shared/styles";
|
||||
import Flex from "~/components/Flex";
|
||||
import Text from "~/components/Text";
|
||||
import { draggableOnDesktop, undraggableOnDesktop } from "~/styles";
|
||||
import Desktop from "~/utils/Desktop";
|
||||
|
||||
export type FullWidthButtonProps = React.ComponentProps<typeof Button> & {
|
||||
export type SidebarButtonProps = React.ComponentProps<typeof Button> & {
|
||||
position: "top" | "bottom";
|
||||
title: React.ReactNode;
|
||||
image: React.ReactNode;
|
||||
minHeight?: number;
|
||||
rounded?: boolean;
|
||||
showDisclosure?: boolean;
|
||||
showMoreMenu?: boolean;
|
||||
onClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const FullWidthButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
FullWidthButtonProps
|
||||
>(function _FullWidthButton(
|
||||
{
|
||||
position = "top",
|
||||
showDisclosure,
|
||||
showMoreMenu,
|
||||
image,
|
||||
title,
|
||||
minHeight = 0,
|
||||
children,
|
||||
...rest
|
||||
}: FullWidthButtonProps,
|
||||
ref
|
||||
) {
|
||||
return (
|
||||
<Container
|
||||
justify="space-between"
|
||||
align="center"
|
||||
shrink={false}
|
||||
$position={position}
|
||||
>
|
||||
<Button
|
||||
{...rest}
|
||||
minHeight={minHeight}
|
||||
as="button"
|
||||
ref={ref}
|
||||
role="button"
|
||||
const SidebarButton = React.forwardRef<HTMLButtonElement, SidebarButtonProps>(
|
||||
function _SidebarButton(
|
||||
{
|
||||
position = "top",
|
||||
showMoreMenu,
|
||||
image,
|
||||
title,
|
||||
minHeight = 0,
|
||||
children,
|
||||
...rest
|
||||
}: SidebarButtonProps,
|
||||
ref
|
||||
) {
|
||||
return (
|
||||
<Container
|
||||
justify="space-between"
|
||||
align="center"
|
||||
shrink={false}
|
||||
$position={position}
|
||||
>
|
||||
<Title gap={8} align="center">
|
||||
{image}
|
||||
{title}
|
||||
</Title>
|
||||
{showDisclosure && <ExpandedIcon />}
|
||||
{showMoreMenu && <MoreIcon />}
|
||||
</Button>
|
||||
{children}
|
||||
</Container>
|
||||
);
|
||||
});
|
||||
<Button
|
||||
{...rest}
|
||||
$minHeight={minHeight}
|
||||
$position={position}
|
||||
as="button"
|
||||
ref={ref}
|
||||
role="button"
|
||||
>
|
||||
<Title gap={8} align="center">
|
||||
{image}
|
||||
{title && <Text as="span">{title}</Text>}
|
||||
</Title>
|
||||
{showMoreMenu && <MoreIcon />}
|
||||
</Button>
|
||||
{children}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const Container = styled(Flex)<{ $position: "top" | "bottom" }>`
|
||||
padding-top: ${(props) =>
|
||||
@@ -67,7 +65,6 @@ const Container = styled(Flex)<{ $position: "top" | "bottom" }>`
|
||||
`;
|
||||
|
||||
const Title = styled(Flex)`
|
||||
color: ${s("text")};
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
text-overflow: ellipsis;
|
||||
@@ -75,19 +72,22 @@ const Title = styled(Flex)`
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const Button = styled(Flex)<{ minHeight: number }>`
|
||||
const Button = styled(Flex)<{
|
||||
$minHeight: number;
|
||||
$position: "top" | "bottom";
|
||||
}>`
|
||||
flex: 1;
|
||||
color: ${s("textTertiary")};
|
||||
align-items: center;
|
||||
padding: 8px 4px;
|
||||
padding: 4px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
margin: 8px 0;
|
||||
border: 0;
|
||||
margin: ${(props) => (props.$position === "top" ? 16 : 8)}px 0;
|
||||
background: none;
|
||||
flex-shrink: 0;
|
||||
min-height: ${(props) => props.minHeight}px;
|
||||
min-height: ${(props) => props.$minHeight}px;
|
||||
|
||||
-webkit-appearance: none;
|
||||
text-decoration: none;
|
||||
@@ -114,4 +114,4 @@ const Button = styled(Flex)<{ minHeight: number }>`
|
||||
}
|
||||
`;
|
||||
|
||||
export default FullWidthButton;
|
||||
export default SidebarButton;
|
||||
@@ -1,106 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled, { css } from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import { s } from "@shared/styles";
|
||||
import Arrow from "~/components/Arrow";
|
||||
import useEventListener from "~/hooks/useEventListener";
|
||||
|
||||
type Props = {
|
||||
direction: "left" | "right";
|
||||
style?: React.CSSProperties;
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
});
|
||||
|
||||
export const ToggleButton = styled.button<{ $direction?: "left" | "right" }>`
|
||||
opacity: 0;
|
||||
background: none;
|
||||
transition: opacity 100ms ease-in-out;
|
||||
transform: translateY(-50%)
|
||||
scaleX(${(props) => (props.$direction === "left" ? 1 : -1)});
|
||||
position: fixed;
|
||||
top: 50vh;
|
||||
padding: 8px;
|
||||
border: 0;
|
||||
pointer-events: none;
|
||||
color: ${s("divider")};
|
||||
|
||||
&:active {
|
||||
color: ${s("sidebarText")};
|
||||
}
|
||||
|
||||
${breakpoint("tablet")`
|
||||
pointer-events: all;
|
||||
cursor: var(--pointer);
|
||||
`}
|
||||
|
||||
@media (hover: none) {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Positioner = styled.div<{ $hovering: boolean }>`
|
||||
display: none;
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: -30px;
|
||||
width: 30px;
|
||||
pointer-events: none;
|
||||
|
||||
&:focus-within ${ToggleButton} {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
${(props) =>
|
||||
props.$hovering &&
|
||||
css`
|
||||
${ToggleButton} {
|
||||
opacity: 1;
|
||||
}
|
||||
`}
|
||||
|
||||
${breakpoint("tablet")`
|
||||
display: block;
|
||||
`}
|
||||
`;
|
||||
|
||||
export default Toggle;
|
||||
15
app/components/Sidebar/components/ToggleButton.tsx
Normal file
15
app/components/Sidebar/components/ToggleButton.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import styled from "styled-components";
|
||||
import { hover } from "~/styles";
|
||||
import SidebarButton from "./SidebarButton";
|
||||
|
||||
const ToggleButton = styled(SidebarButton)`
|
||||
opacity: 0;
|
||||
transition: opacity 100ms ease-in-out;
|
||||
|
||||
&:${hover},
|
||||
&:active {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
export default ToggleButton;
|
||||
Reference in New Issue
Block a user