Skip to content

Commit

Permalink
feat(Frontend): Add FloatingSidebarButton component (#276)
Browse files Browse the repository at this point in the history
* Organize test setup files

* Refactor TestApp component

* Add FloatingSidebarButton component
  • Loading branch information
Kohei Asai authored Mar 27, 2021
1 parent 7390fd7 commit 9c0ea75
Show file tree
Hide file tree
Showing 10 changed files with 673 additions and 63 deletions.
124 changes: 124 additions & 0 deletions components/floating-sidebar-button.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { act, fireEvent, render } from "@testing-library/react";
import * as React from "react";
import { TestApp, TestAppElement } from "../core/test-app";
import { FloatingSidebarButton } from "./floating-sidebar-button";

describe("<FloatingSidebarButton>", () => {
it("renders a button in the document body", () => {
const { getByTestId } = render(
<TestApp>
<FloatingSidebarButton
content={<div data-testid="content" />}
data-testid="floating-sidebar-button"
/>
</TestApp>
);

expect(getByTestId("floating-sidebar-button")).toBeInTheDocument();
});

it("renders a floating sidebar in the document but it's hidden", () => {
const { getByTestId } = render(
<TestApp>
<FloatingSidebarButton content={<div data-testid="content" />} />
</TestApp>
);

expect(getByTestId("floating-sidebar")).toBeInTheDocument();
expect(getByTestId("floating-sidebar")).not.toBeVisible();
});

it("shows the floating sidebar when the button is clicked", () => {
const { getByTestId } = render(
<TestApp>
<FloatingSidebarButton
content={<div data-testid="content" />}
data-testid="floating-sidebar-button"
/>
</TestApp>
);

act(() => {
fireEvent.click(getByTestId("floating-sidebar-button"));
});

expect(getByTestId("floating-sidebar")).toBeVisible();
});

it("hides the floating sidebar when the button is clicked while the sidebar is shown", () => {
const { getByTestId } = render(
<TestApp>
<FloatingSidebarButton
content={<div data-testid="content" />}
data-testid="floating-sidebar-button"
/>
</TestApp>
);

act(() => {
fireEvent.click(getByTestId("floating-sidebar-button"));
});
act(() => {
fireEvent.click(getByTestId("floating-sidebar-button"));
});

expect(getByTestId("floating-sidebar")).not.toBeVisible();
});

describe("props.closeOnHashChange = true", () => {
it("hides the floating sidebar when a route change caused", () => {
let testAppElement!: TestAppElement;
const { getByTestId } = render(
<TestApp
ref={(el) => {
testAppElement = el!;
}}
>
<FloatingSidebarButton
content={<div data-testid="content" />}
data-testid="floating-sidebar-button"
/>
</TestApp>
);

act(() => {
fireEvent.click(getByTestId("floating-sidebar-button"));
});

act(() => {
testAppElement.emulateRouteChangeComplete();
});

expect(getByTestId("floating-sidebar")).not.toBeVisible();
});
});

describe("props.closeOnHashChange = false", () => {
it("keeps the floating sidebar shown even when a route change caused", () => {
let testAppElement!: TestAppElement;
const { getByTestId } = render(
<TestApp
ref={(el) => {
testAppElement = el!;
}}
>
<FloatingSidebarButton
content={<div data-testid="content" />}
closeOnRouteChange={false}
data-testid="floating-sidebar-button"
/>
</TestApp>
);

act(() => {
fireEvent.click(getByTestId("floating-sidebar-button"));
});

act(() => {
testAppElement.emulateRouteChangeComplete();
});

expect(getByTestId("floating-sidebar")).toBeVisible();
});
});
});
36 changes: 36 additions & 0 deletions components/floating-sidebar-button.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Story, Meta } from "@storybook/react";
import * as React from "react";
import {
FloatingSidebarButton,
FloatingSidebarButtonProps,
} from "./floating-sidebar-button";

export default {
title: "Components/FloatingSidebarButton",
component: FloatingSidebarButton,
argTypes: {
className: { control: false },
},
args: {
content: "Hello!",
},
parameters: {
layout: "fullscreen",
},
} as Meta;

export const Example: Story<FloatingSidebarButtonProps> = (props) => (
<>
<img
src="https://i.picsum.photos/id/515/1280/1280.jpg?hmac=wpcdFefrM6BhacWw0k0ZF33nSoTzrRs5amNMhTKhl-o"
style={{
display: "block",
width: "100vw",
height: "100vh",
objectFit: "cover",
}}
/>

<FloatingSidebarButton {...props} />
</>
);
116 changes: 116 additions & 0 deletions components/floating-sidebar-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { css, cx } from "@linaria/core";
import { useRouter } from "next/router";
import * as React from "react";
import * as ReactDOM from "react-dom";
import { List as ListIcon, X as XIcon } from "react-feather";
import { Button, ButtonSize, ButtonVariant } from "./button";

export interface FloatingSidebarButtonProps extends React.Attributes {
closeOnRouteChange?: boolean;
content: React.ReactNode;
className?: string;
style?: React.CSSProperties;
}

export const FloatingSidebarButton: React.VFC<FloatingSidebarButtonProps> = ({
closeOnRouteChange = true,
content,
className,
...props
}) => {
const router = useRouter();
const floatingSidebarRef = React.useRef<HTMLDivElement>(null);
const [isMenuOpen, setMenuOpen] = React.useState(false);

React.useEffect(() => {
const onRouteChangeComplete = () => {
setMenuOpen(false);

setTimeout(() => {
floatingSidebarRef.current?.scrollTo(0, 0);
}, 300);
};

if (closeOnRouteChange) {
router.events.on("routeChangeComplete", onRouteChangeComplete);
}

return () => {
if (closeOnRouteChange) {
router.events.off("routeChangeComplete", onRouteChangeComplete);
}
};
}, [closeOnRouteChange]);

return (
<>
{typeof globalThis.window !== "undefined"
? ReactDOM.createPortal(
<Button
variant={ButtonVariant.inverted}
size={ButtonSize.xl}
icon={isMenuOpen ? <XIcon /> : <ListIcon />}
onClick={() => setMenuOpen((isMenuOpen) => !isMenuOpen)}
className={cx(
css`
position: fixed;
bottom: calc(
env(safe-area-inset-bottom, 0px) + var(--space-md)
);
right: calc(
env(safe-area-inset-right, 0px) + var(--space-md)
);
z-index: 100000000;
touch-action: manipulation;
`,
className
)}
{...props}
/>,
globalThis.document.body
)
: null}

{typeof globalThis.window !== "undefined"
? ReactDOM.createPortal(
<div
className={css`
position: fixed;
top: 0;
width: 100vw;
max-width: 480px;
height: 100vh;
padding-block-start: var(--space-lg);
padding-block-end: 80px;
background-color: var(--color-bg-frosted);
backdrop-filter: blur(8px);
overflow-x: hidden;
overflow-y: scroll;
z-index: 10000000;
transition: opacity 300ms ease-in-out 0ms,
visibility 300ms ease-in-out 0ms, right 300ms ease-in-out 0ms;
`}
style={
isMenuOpen
? {
right: 0,
visibility: "visible",
opacity: 1,
}
: {
right: -32,
visibility: "hidden",
opacity: 0,
}
}
ref={floatingSidebarRef}
data-testid="floating-sidebar"
>
{content}
</div>,
globalThis.document.body
)
: null}
</>
);
};
Loading

1 comment on commit 9c0ea75

@vercel
Copy link

@vercel vercel bot commented on 9c0ea75 Mar 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.