-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(web): Add popover and basic content components (#580)
- Loading branch information
Showing
14 changed files
with
488 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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], | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}} | ||
/> | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
52
web/src/beta/components/PopoverMenuContent/index.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"), | ||
}, | ||
], | ||
}, | ||
}; |
Oops, something went wrong.