Skip to content

Commit

Permalink
chore(web): Add popover and basic content components (#580)
Browse files Browse the repository at this point in the history
  • Loading branch information
isoppp authored Jul 19, 2023
1 parent 7439637 commit 53bf82f
Show file tree
Hide file tree
Showing 14 changed files with 488 additions and 16 deletions.
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
"@aws-amplify/ui-react": "5.0.2",
"@emotion/react": "11.11.0",
"@emotion/styled": "11.11.0",
"@floating-ui/react-dom": "2.0.0",
"@floating-ui/react": "0.24.7",
"@mapbox/vector-tile": "1.3.1",
"@monaco-editor/react": "4.5.1",
"@popperjs/core": "2.11.7",
Expand Down
11 changes: 11 additions & 0 deletions web/src/beta/components/Icon/Icons/copy.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions web/src/beta/components/Icon/Icons/gearSix.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions web/src/beta/components/Icon/Icons/pencilSimple.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions web/src/beta/components/Icon/Icons/trash.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions web/src/beta/components/Icon/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ import Workspaces from "./Icons/workspaces.svg";
import Square from "./Icons/square.svg";
import Swiper from "./Icons/swiper.svg";
import Book from "./Icons/book.svg";
import Copy from "./Icons/copy.svg";
import GearSix from "./Icons/gearSix.svg";
import PencilSimple from "./Icons/pencilSimple.svg";
import Trash from "./Icons/trash.svg";

// Plus
import Plus from "./Icons/plus.svg";
Expand Down Expand Up @@ -111,4 +115,8 @@ export default {
checkmark: CheckMark,
logo: Logo,
logoColorful: LogoColorful,
copy: Copy,
gearSix: GearSix,
pencilSimple: PencilSimple,
trash: Trash,
};
17 changes: 17 additions & 0 deletions web/src/beta/components/Popover/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { createContext, useContext } from "react";

import usePopover from "@reearth/beta/components/Popover/hooks";

type ContextType = ReturnType<typeof usePopover> | null;

export const PopoverContext = createContext<ContextType>(null);

export const usePopoverContext = () => {
const context = useContext(PopoverContext);

if (context === null) {
throw new Error("Popover components must be wrapped in <Popover.Root />");
}

return context;
};
65 changes: 65 additions & 0 deletions web/src/beta/components/Popover/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {
autoUpdate,
flip,
offset,
shift,
useClick,
useDismiss,
useFloating,
useInteractions,
useRole,
} from "@floating-ui/react";
import * as React from "react";

import { PopoverOptions } from "./types";

export default function usePopover({
initialOpen = false,
placement = "bottom",
modal,
offset: offsetProps,
open: controlledOpen,
onOpenChange: setControlledOpen,
}: PopoverOptions = {}) {
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(initialOpen);

const open = controlledOpen ?? uncontrolledOpen;
const setOpen = setControlledOpen ?? setUncontrolledOpen;

const data = useFloating({
placement,
open,
onOpenChange: setOpen,
whileElementsMounted: autoUpdate,
middleware: [
offset(offsetProps ?? 4),
flip({
crossAxis: placement.includes("-"),
fallbackAxisSideDirection: "end",
padding: 4,
}),
shift({ padding: 4 }),
],
});

const context = data.context;

const click = useClick(context, {
enabled: controlledOpen == null,
});
const dismiss = useDismiss(context);
const role = useRole(context);

const interactions = useInteractions([click, dismiss, role]);

return React.useMemo(
() => ({
open,
setOpen,
...interactions,
...data,
modal,
}),
[open, setOpen, interactions, data, modal],
);
}
75 changes: 75 additions & 0 deletions web/src/beta/components/Popover/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Meta, StoryObj } from "@storybook/react";

import * as Popover from "./";

export default {
component: Popover.Provider,
} as Meta;

type Story = StoryObj<typeof Popover.Provider>;

export const Controlled: Story = {
render: args => {
return <Popover.Provider {...args} />;
},
args: {
children: (
<>
<Popover.Trigger>
<div style={{ background: "gray" }}>Trigger(cannot toggle by click)</div>
</Popover.Trigger>
<Popover.Content>
<div>
If you pass open or onOpenChange, you can handle open state by yourself.
<br />
Which means the PopoverClose component does not change state automatically but still
causes event.
<br />
This component set as top placement, but it can automatically adjust position to bottom.
</div>
<Popover.Close style={{ color: "inherit" }}>Close</Popover.Close>
</Popover.Content>
</>
),
open: true,
initialOpen: false,
placement: "top",
modal: true,
},
};

export const Uncontrolled: Story = {
render: args => {
return (
<div style={{ maxWidth: "200px", margin: "0 auto" }}>
<Popover.Provider {...args} />
</div>
);
},
args: {
children: (
<>
<Popover.Trigger>
<div style={{ background: "gray" }}>Trigger</div>
</Popover.Trigger>
<Popover.Content style={{ background: "gray" }}>
<div>
If you do not pass both open and onOpenChange, automatically managed open state by this
component.
</div>
<Popover.Close style={{ color: "inherit" }}>Close</Popover.Close>
</Popover.Content>
</>
),
open: undefined,
onOpenChange: undefined,
initialOpen: true,
placement: "bottom",
offset: {
mainAxis: 20,
crossAxis: 300,
alignmentAxis: 0,
},
modal: true,
},
};
104 changes: 104 additions & 0 deletions web/src/beta/components/Popover/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import {
useMergeRefs,
FloatingPortal,
FloatingFocusManager,
useTransitionStyles,
} from "@floating-ui/react";
import * as React from "react";

import { PopoverContext, usePopoverContext } from "@reearth/beta/components/Popover/context";

import usePopover from "./hooks";
import { PopoverOptions } from "./types";

// Basic structure comes from official example https://floating-ui.com/docs/react-examples .
export function Provider({
children,
modal = false,
...restOptions
}: {
children: React.ReactNode;
} & PopoverOptions) {
const popover = usePopover({ modal, ...restOptions });
return <PopoverContext.Provider value={popover}>{children}</PopoverContext.Provider>;
}

type TriggerProps = {
children: React.ReactNode;
asChild?: boolean;
};

export const Trigger = React.forwardRef<HTMLElement, React.HTMLProps<HTMLElement> & TriggerProps>(
function Trigger({ children, asChild = false, ...props }, propRef) {
const context = usePopoverContext();
const childrenRef = (children as any).ref;
const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef]);

// `asChild` allows the user to pass any element as the anchor
if (asChild && React.isValidElement(children)) {
return React.cloneElement(
children,
context.getReferenceProps({
ref,
...props,
...children.props,
"data-state": context.open ? "open" : "closed",
}),
);
}

return (
<button
ref={ref}
type="button"
// The user can style the trigger based on the state
data-state={context.open ? "open" : "closed"}
{...context.getReferenceProps(props)}>
{children}
</button>
);
},
);

export const Content = React.forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement>>(
function Content({ style, ...props }, propRef) {
const { context: floatingContext, ...context } = usePopoverContext();
const ref = useMergeRefs([context.refs.setFloating, propRef]);
const { isMounted, styles: transitionStyles } = useTransitionStyles(floatingContext, {
duration: 50,
});

if (!isMounted) return null;

return (
<FloatingPortal>
<FloatingFocusManager context={floatingContext} modal={context.modal}>
<div
ref={ref}
style={{ ...context.floatingStyles, ...transitionStyles, ...style }}
{...context.getFloatingProps(props)}>
{props.children}
</div>
</FloatingFocusManager>
</FloatingPortal>
);
},
);

export const Close = React.forwardRef<
HTMLButtonElement,
React.ButtonHTMLAttributes<HTMLButtonElement>
>(function PopoverClose(props, ref) {
const { setOpen } = usePopoverContext();
return (
<button
type="button"
ref={ref}
{...props}
onClick={event => {
props.onClick?.(event);
setOpen(false);
}}
/>
);
});
10 changes: 10 additions & 0 deletions web/src/beta/components/Popover/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Placement, OffsetOptions } from "@floating-ui/react";

export type PopoverOptions = {
initialOpen?: boolean;
placement?: Placement;
modal?: boolean; // @see https://floating-ui.com/docs/floatingfocusmanager#modal
open?: boolean;
offset?: OffsetOptions; // @see https://floating-ui.com/docs/offset
onOpenChange?: (open: boolean) => void;
};
52 changes: 52 additions & 0 deletions web/src/beta/components/PopoverMenuContent/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { action } from "@storybook/addon-actions";
import { Meta, StoryObj } from "@storybook/react";

import PopoverContent from ".";

const meta: Meta<typeof PopoverContent> = {
component: PopoverContent,
argTypes: {
size: {
control: "radio",
options: ["sm", "md"],
},
},
};

export default meta;

type Story = StoryObj<typeof PopoverContent>;

export const Default: Story = {
args: {
width: "200px",
size: "md",
items: [
{
icon: "copy",
name: "Name",
onClick: action("onClickItem"),
isSelected: true,
},
{
icon: "pencilSimple",
name: "NameNameNameNameNameNameName",
onClick: action("onClickItem"),
},
{
name: "NameNameNameNameNameNameName",
onClick: action("onClickItem"),
},
{
icon: "trash",
name: "Name",
onClick: action("onClickItem"),
},
{
icon: "gearSix",
name: "NameNameNameNameNameNameName",
onClick: action("onClickItem"),
},
],
},
};
Loading

0 comments on commit 53bf82f

Please sign in to comment.