mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
feat(popover): component created, a11y passing, tests passing
This commit is contained in:
parent
f51d19b09b
commit
fdaaad4210
@ -1,11 +1,21 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {render} from "@testing-library/react";
|
import {render, fireEvent, act} from "@testing-library/react";
|
||||||
|
import {Button} from "@nextui-org/button";
|
||||||
|
|
||||||
import {Popover} from "../src";
|
import {Popover, PopoverContent, PopoverTrigger} from "../src";
|
||||||
|
|
||||||
describe("Popover", () => {
|
describe("Popover", () => {
|
||||||
it("should render correctly", () => {
|
it("should render correctly", () => {
|
||||||
const wrapper = render(<Popover />);
|
const wrapper = render(
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<button>Open popover</button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent>
|
||||||
|
<p>This is the content of the popover.</p>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>,
|
||||||
|
);
|
||||||
|
|
||||||
expect(() => wrapper.unmount()).not.toThrow();
|
expect(() => wrapper.unmount()).not.toThrow();
|
||||||
});
|
});
|
||||||
@ -13,7 +23,105 @@ describe("Popover", () => {
|
|||||||
it("ref should be forwarded", () => {
|
it("ref should be forwarded", () => {
|
||||||
const ref = React.createRef<HTMLDivElement>();
|
const ref = React.createRef<HTMLDivElement>();
|
||||||
|
|
||||||
render(<Popover ref={ref} />);
|
render(
|
||||||
|
<Popover ref={ref}>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<button>Open popover</button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent>
|
||||||
|
<p>This is the content of the popover.</p>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>,
|
||||||
|
);
|
||||||
expect(ref.current).not.toBeNull();
|
expect(ref.current).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should hide the popover when pressing the escape key", () => {
|
||||||
|
const onClose = jest.fn();
|
||||||
|
|
||||||
|
const wrapper = render(
|
||||||
|
<Popover isOpen onOpenChange={(isOpen) => (!isOpen ? onClose() : undefined)}>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<button>Open popover</button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent data-testid="content-test">
|
||||||
|
<p>This is the content of the popover.</p>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = wrapper.getByTestId("content-test");
|
||||||
|
|
||||||
|
fireEvent.keyDown(content, {key: "Escape"});
|
||||||
|
expect(onClose).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should still hide the popover when pressing the escape key ", () => {
|
||||||
|
const onClose = jest.fn();
|
||||||
|
|
||||||
|
const wrapper = render(
|
||||||
|
<Popover isOpen onOpenChange={(isOpen) => (!isOpen ? onClose() : undefined)}>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<button>Open popover</button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent data-testid="content-test">
|
||||||
|
<p>This is the content of the popover.</p>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = wrapper.getByTestId("content-test");
|
||||||
|
|
||||||
|
fireEvent.keyDown(content, {key: "Escape"});
|
||||||
|
expect(onClose).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should hide the popover on blur ", () => {
|
||||||
|
const onClose = jest.fn();
|
||||||
|
|
||||||
|
const wrapper = render(
|
||||||
|
<Popover isOpen onOpenChange={(isOpen) => (!isOpen ? onClose() : undefined)}>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<button>Open popover</button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent data-testid="content-test">
|
||||||
|
<p>This is the content of the popover.</p>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = wrapper.getByTestId("content-test");
|
||||||
|
|
||||||
|
fireEvent.blur(content);
|
||||||
|
expect(onClose).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with NextUI button", () => {
|
||||||
|
const onClose = jest.fn();
|
||||||
|
|
||||||
|
const wrapper = render(
|
||||||
|
<Popover onOpenChange={(isOpen) => (!isOpen ? onClose() : undefined)}>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button data-testid="trigger-test">Open popover</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent>
|
||||||
|
<p>This is the content of the popover.</p>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const trigger = wrapper.getByTestId("trigger-test");
|
||||||
|
|
||||||
|
// open popover
|
||||||
|
act(() => {
|
||||||
|
trigger.click();
|
||||||
|
});
|
||||||
|
expect(onClose).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
// close popover
|
||||||
|
act(() => {
|
||||||
|
trigger.click();
|
||||||
|
});
|
||||||
|
expect(onClose).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -10,5 +10,8 @@ export type {PopoverContentProps} from "./popover-content";
|
|||||||
// export hooks
|
// export hooks
|
||||||
export {usePopover} from "./use-popover";
|
export {usePopover} from "./use-popover";
|
||||||
|
|
||||||
|
// export context
|
||||||
|
export * from "./popover-context";
|
||||||
|
|
||||||
// export components
|
// export components
|
||||||
export {Popover, PopoverTrigger, PopoverContent};
|
export {Popover, PopoverTrigger, PopoverContent};
|
||||||
|
|||||||
@ -1,14 +1,17 @@
|
|||||||
import {ReactNode, useMemo, useCallback} from "react";
|
import type {AriaDialogProps} from "@react-aria/dialog";
|
||||||
|
|
||||||
|
import {ReactNode, useMemo, useRef} from "react";
|
||||||
import {forwardRef} from "@nextui-org/system";
|
import {forwardRef} from "@nextui-org/system";
|
||||||
import {DismissButton} from "@react-aria/overlays";
|
import {DismissButton} from "@react-aria/overlays";
|
||||||
import {TRANSITION_VARIANTS} from "@nextui-org/framer-transitions";
|
import {TRANSITION_VARIANTS} from "@nextui-org/framer-transitions";
|
||||||
import {FocusScope} from "@react-aria/focus";
|
|
||||||
import {motion} from "framer-motion";
|
import {motion} from "framer-motion";
|
||||||
import {getTransformOrigins} from "@nextui-org/aria-utils";
|
import {getTransformOrigins} from "@nextui-org/aria-utils";
|
||||||
|
import {useDialog} from "@react-aria/dialog";
|
||||||
|
import {mergeProps} from "@react-aria/utils";
|
||||||
|
|
||||||
import {usePopoverContext} from "./popover-context";
|
import {usePopoverContext} from "./popover-context";
|
||||||
|
|
||||||
export interface PopoverContentProps {
|
export interface PopoverContentProps extends AriaDialogProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,19 +20,25 @@ const PopoverContent = forwardRef<PopoverContentProps, "section">((props, _) =>
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
Component: OverlayComponent,
|
Component: OverlayComponent,
|
||||||
isOpen,
|
|
||||||
placement,
|
placement,
|
||||||
showArrow,
|
showArrow,
|
||||||
motionProps,
|
motionProps,
|
||||||
disableAnimation,
|
disableAnimation,
|
||||||
getPopoverProps,
|
getPopoverProps,
|
||||||
getArrowProps,
|
getArrowProps,
|
||||||
|
getDialogProps,
|
||||||
onClose,
|
onClose,
|
||||||
} = usePopoverContext();
|
} = usePopoverContext();
|
||||||
|
|
||||||
const Component = as || OverlayComponent || "div";
|
const Component = as || OverlayComponent || "div";
|
||||||
|
|
||||||
const {style, className, ...otherPopoverProps} = getPopoverProps(otherProps);
|
const dialogRef = useRef(null);
|
||||||
|
const {dialogProps} = useDialog(
|
||||||
|
{
|
||||||
|
role: "dialog",
|
||||||
|
},
|
||||||
|
dialogRef,
|
||||||
|
);
|
||||||
|
|
||||||
const arrowContent = useMemo(() => {
|
const arrowContent = useMemo(() => {
|
||||||
if (!showArrow) return null;
|
if (!showArrow) return null;
|
||||||
@ -37,42 +46,24 @@ const PopoverContent = forwardRef<PopoverContentProps, "section">((props, _) =>
|
|||||||
return <span {...getArrowProps()} />;
|
return <span {...getArrowProps()} />;
|
||||||
}, [showArrow, getArrowProps]);
|
}, [showArrow, getArrowProps]);
|
||||||
|
|
||||||
const ContentWrapper = useCallback(
|
const content = (
|
||||||
({children}: {children: ReactNode}) => {
|
<>
|
||||||
return (
|
<DismissButton onDismiss={onClose} />
|
||||||
<FocusScope restoreFocus>
|
<Component {...getDialogProps(mergeProps(dialogProps, otherProps))} ref={dialogRef}>
|
||||||
<Component className={className}>
|
{children}
|
||||||
<DismissButton onDismiss={onClose} />
|
{arrowContent}
|
||||||
{children}
|
</Component>
|
||||||
{arrowContent}
|
<DismissButton onDismiss={onClose} />
|
||||||
<DismissButton onDismiss={onClose} />
|
</>
|
||||||
</Component>
|
|
||||||
</FocusScope>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[Component, className, onClose, arrowContent],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const visibility = useMemo(() => {
|
|
||||||
if (disableAnimation) return isOpen ? "visible" : "hidden";
|
|
||||||
|
|
||||||
return "visible";
|
|
||||||
}, [disableAnimation, isOpen]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div {...getPopoverProps()}>
|
||||||
{...otherPopoverProps}
|
|
||||||
style={{
|
|
||||||
...style,
|
|
||||||
visibility,
|
|
||||||
outline: "none",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{disableAnimation ? (
|
{disableAnimation ? (
|
||||||
<ContentWrapper>{children}</ContentWrapper>
|
content
|
||||||
) : (
|
) : (
|
||||||
<motion.div
|
<motion.div
|
||||||
animate={isOpen ? "enter" : "exit"}
|
animate="enter"
|
||||||
exit="exit"
|
exit="exit"
|
||||||
initial="exit"
|
initial="exit"
|
||||||
style={{
|
style={{
|
||||||
@ -81,7 +72,7 @@ const PopoverContent = forwardRef<PopoverContentProps, "section">((props, _) =>
|
|||||||
variants={TRANSITION_VARIANTS.scaleSpring}
|
variants={TRANSITION_VARIANTS.scaleSpring}
|
||||||
{...motionProps}
|
{...motionProps}
|
||||||
>
|
>
|
||||||
<ContentWrapper>{children}</ContentWrapper>
|
{content}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import {forwardRef} from "@nextui-org/system";
|
import {forwardRef} from "@nextui-org/system";
|
||||||
import {OverlayContainer} from "@react-aria/overlays";
|
|
||||||
import {Children, ReactNode} from "react";
|
import {Children, ReactNode} from "react";
|
||||||
|
import {AnimatePresence} from "framer-motion";
|
||||||
|
import {Overlay} from "@react-aria/overlays";
|
||||||
|
|
||||||
import {UsePopoverProps, usePopover} from "./use-popover";
|
import {UsePopoverProps, usePopover} from "./use-popover";
|
||||||
import {PopoverProvider} from "./popover-context";
|
import {PopoverProvider} from "./popover-context";
|
||||||
@ -19,12 +20,16 @@ const Popover = forwardRef<PopoverProps, "div">((props, ref) => {
|
|||||||
|
|
||||||
const [trigger, content] = Children.toArray(children);
|
const [trigger, content] = Children.toArray(children);
|
||||||
|
|
||||||
const mountOverlay = context.isOpen;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoverProvider value={context}>
|
<PopoverProvider value={context}>
|
||||||
{trigger}
|
{trigger}
|
||||||
{mountOverlay && <OverlayContainer>{content}</OverlayContainer>}
|
{context.disableAnimation && context.isOpen ? (
|
||||||
|
<Overlay>{content}</Overlay>
|
||||||
|
) : (
|
||||||
|
<AnimatePresence initial={false}>
|
||||||
|
{context.isOpen ? <Overlay>{content}</Overlay> : null}
|
||||||
|
</AnimatePresence>
|
||||||
|
)}
|
||||||
</PopoverProvider>
|
</PopoverProvider>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,35 +1,32 @@
|
|||||||
import type {PopoverVariantProps, SlotsToClasses, PopoverSlots} from "@nextui-org/theme";
|
import type {PopoverVariantProps, SlotsToClasses, PopoverSlots} from "@nextui-org/theme";
|
||||||
import type {HTMLMotionProps} from "framer-motion";
|
import type {HTMLMotionProps} from "framer-motion";
|
||||||
import type {OverlayPlacement, OverlayOptions} from "@nextui-org/aria-utils";
|
import type {OverlayPlacement} from "@nextui-org/aria-utils";
|
||||||
import type {RefObject, Ref} from "react";
|
import type {RefObject, Ref} from "react";
|
||||||
|
|
||||||
import {useOverlayTriggerState} from "@react-stately/overlays";
|
import {useOverlayTriggerState} from "@react-stately/overlays";
|
||||||
import {useFocusRing} from "@react-aria/focus";
|
import {useFocusRing} from "@react-aria/focus";
|
||||||
import {
|
import {
|
||||||
AriaOverlayProps,
|
AriaPopoverProps,
|
||||||
useOverlayTrigger,
|
useOverlayTrigger,
|
||||||
useOverlayPosition,
|
usePopover as useReactAriaPopover,
|
||||||
useOverlay,
|
|
||||||
useModal,
|
|
||||||
} from "@react-aria/overlays";
|
} from "@react-aria/overlays";
|
||||||
import {useDialog} from "@react-aria/dialog";
|
|
||||||
import {OverlayTriggerProps} from "@react-types/overlays";
|
import {OverlayTriggerProps} from "@react-types/overlays";
|
||||||
import {HTMLNextUIProps, mapPropsVariants, PropGetter} from "@nextui-org/system";
|
import {HTMLNextUIProps, mapPropsVariants, PropGetter} from "@nextui-org/system";
|
||||||
import {toReactAriaPlacement, getArrowPlacement} from "@nextui-org/aria-utils";
|
import {toReactAriaPlacement, getArrowPlacement} from "@nextui-org/aria-utils";
|
||||||
import {popover} from "@nextui-org/theme";
|
import {popover} from "@nextui-org/theme";
|
||||||
import {chain, mergeProps, mergeRefs} from "@react-aria/utils";
|
import {mergeProps, mergeRefs} from "@react-aria/utils";
|
||||||
import {createDOMRef} from "@nextui-org/dom-utils";
|
import {createDOMRef} from "@nextui-org/dom-utils";
|
||||||
import {ReactRef, clsx} from "@nextui-org/shared-utils";
|
import {ReactRef, clsx} from "@nextui-org/shared-utils";
|
||||||
import {useId, useMemo, useCallback, useImperativeHandle, useRef} from "react";
|
import {useId, useMemo, useCallback, useImperativeHandle, useRef} from "react";
|
||||||
|
|
||||||
export interface Props extends HTMLNextUIProps<"div", PopoverVariantProps> {
|
export interface Props extends HTMLNextUIProps<"div"> {
|
||||||
/**
|
/**
|
||||||
* Ref to the DOM node.
|
* Ref to the DOM node.
|
||||||
*/
|
*/
|
||||||
ref?: ReactRef<HTMLElement | null>;
|
ref?: ReactRef<HTMLElement | null>;
|
||||||
/**
|
/**
|
||||||
* A ref for the scrollable region within the overlay.
|
* A ref for the scrollable region within the overlay.
|
||||||
* @default overlayRef
|
* @default popoverRef
|
||||||
*/
|
*/
|
||||||
scrollRef?: RefObject<HTMLElement>;
|
scrollRef?: RefObject<HTMLElement>;
|
||||||
/**
|
/**
|
||||||
@ -41,6 +38,11 @@ export interface Props extends HTMLNextUIProps<"div", PopoverVariantProps> {
|
|||||||
* @default 'top'
|
* @default 'top'
|
||||||
*/
|
*/
|
||||||
placement?: OverlayPlacement;
|
placement?: OverlayPlacement;
|
||||||
|
/**
|
||||||
|
* Whether the element should render an arrow.
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
showArrow?: boolean;
|
||||||
/**
|
/**
|
||||||
* Type of overlay that is opened by the trigger.
|
* Type of overlay that is opened by the trigger.
|
||||||
*/
|
*/
|
||||||
@ -62,11 +64,12 @@ export interface Props extends HTMLNextUIProps<"div", PopoverVariantProps> {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
styles?: SlotsToClasses<PopoverSlots>;
|
styles?: SlotsToClasses<PopoverSlots>;
|
||||||
/** Handler that is called when the overlay should close. */
|
|
||||||
onClose?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UsePopoverProps = Props & AriaOverlayProps & OverlayTriggerProps & OverlayOptions;
|
export type UsePopoverProps = Props &
|
||||||
|
Omit<AriaPopoverProps, "placement" | "triggerRef" | "popoverRef"> &
|
||||||
|
OverlayTriggerProps &
|
||||||
|
PopoverVariantProps;
|
||||||
|
|
||||||
export function usePopover(originalProps: UsePopoverProps) {
|
export function usePopover(originalProps: UsePopoverProps) {
|
||||||
const [props, variantProps] = mapPropsVariants(originalProps, popover.variantKeys);
|
const [props, variantProps] = mapPropsVariants(originalProps, popover.variantKeys);
|
||||||
@ -87,29 +90,25 @@ export function usePopover(originalProps: UsePopoverProps) {
|
|||||||
showArrow = false,
|
showArrow = false,
|
||||||
offset = 7,
|
offset = 7,
|
||||||
crossOffset = 0,
|
crossOffset = 0,
|
||||||
isDismissable = true,
|
isKeyboardDismissDisabled,
|
||||||
shouldCloseOnBlur = true,
|
|
||||||
isKeyboardDismissDisabled = true,
|
|
||||||
shouldCloseOnInteractOutside,
|
|
||||||
motionProps,
|
motionProps,
|
||||||
className,
|
className,
|
||||||
styles,
|
styles,
|
||||||
onClose,
|
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const Component = as || "div";
|
const Component = as || "div";
|
||||||
const popoverId = useId();
|
const popoverId = useId();
|
||||||
|
|
||||||
const overlayRef = useRef<HTMLDivElement>(null);
|
const popoverRef = useRef<HTMLDivElement>(null);
|
||||||
const domTriggerRef = useRef<HTMLElement>(null);
|
const domTriggerRef = useRef<HTMLElement>(null);
|
||||||
|
|
||||||
const triggerRef = triggerRefProp || domTriggerRef;
|
const triggerRef = triggerRefProp || domTriggerRef;
|
||||||
|
|
||||||
// Sync ref with overlayRef from passed ref.
|
// Sync ref with popoverRef from passed ref.
|
||||||
useImperativeHandle(ref, () =>
|
useImperativeHandle(ref, () =>
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
createDOMRef(overlayRef),
|
createDOMRef(popoverRef),
|
||||||
);
|
);
|
||||||
|
|
||||||
const state = useOverlayTriggerState({
|
const state = useOverlayTriggerState({
|
||||||
@ -118,47 +117,25 @@ export function usePopover(originalProps: UsePopoverProps) {
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
});
|
});
|
||||||
|
|
||||||
const {triggerProps, overlayProps: overlayTriggerProps} = useOverlayTrigger(
|
const {popoverProps, arrowProps, placement} = useReactAriaPopover(
|
||||||
{type: triggerType},
|
|
||||||
state,
|
|
||||||
triggerRef,
|
|
||||||
);
|
|
||||||
|
|
||||||
const {overlayProps: positionProps, arrowProps, placement} = useOverlayPosition({
|
|
||||||
overlayRef,
|
|
||||||
scrollRef,
|
|
||||||
isOpen: isOpen,
|
|
||||||
targetRef: triggerRef,
|
|
||||||
placement: toReactAriaPlacement(placementProp),
|
|
||||||
offset: showArrow ? offset + 3 : offset,
|
|
||||||
crossOffset,
|
|
||||||
shouldFlip,
|
|
||||||
containerPadding,
|
|
||||||
});
|
|
||||||
|
|
||||||
const {overlayProps} = useOverlay(
|
|
||||||
{
|
{
|
||||||
isOpen: state.isOpen,
|
triggerRef,
|
||||||
onClose: chain(state.close, onClose),
|
popoverRef,
|
||||||
isDismissable,
|
placement: toReactAriaPlacement(placementProp),
|
||||||
shouldCloseOnBlur,
|
offset: showArrow ? offset + 3 : offset,
|
||||||
|
scrollRef,
|
||||||
|
crossOffset,
|
||||||
|
shouldFlip,
|
||||||
|
containerPadding,
|
||||||
isKeyboardDismissDisabled,
|
isKeyboardDismissDisabled,
|
||||||
shouldCloseOnInteractOutside,
|
|
||||||
},
|
},
|
||||||
overlayRef,
|
state,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const {triggerProps} = useOverlayTrigger({type: triggerType}, state, triggerRef);
|
||||||
|
|
||||||
const {isFocusVisible, focusProps} = useFocusRing();
|
const {isFocusVisible, focusProps} = useFocusRing();
|
||||||
|
|
||||||
const {modalProps} = useModal({isDisabled: true});
|
|
||||||
|
|
||||||
const {dialogProps} = useDialog(
|
|
||||||
{
|
|
||||||
role: "dialog",
|
|
||||||
},
|
|
||||||
overlayRef,
|
|
||||||
);
|
|
||||||
|
|
||||||
const slots = useMemo(
|
const slots = useMemo(
|
||||||
() =>
|
() =>
|
||||||
popover({
|
popover({
|
||||||
@ -171,21 +148,20 @@ export function usePopover(originalProps: UsePopoverProps) {
|
|||||||
const baseStyles = clsx(styles?.base, className);
|
const baseStyles = clsx(styles?.base, className);
|
||||||
|
|
||||||
const getPopoverProps: PropGetter = (props = {}) => ({
|
const getPopoverProps: PropGetter = (props = {}) => ({
|
||||||
ref: overlayRef,
|
ref: popoverRef,
|
||||||
...mergeProps(
|
...mergeProps(popoverProps, otherProps, props),
|
||||||
overlayTriggerProps,
|
|
||||||
overlayProps,
|
|
||||||
modalProps,
|
|
||||||
dialogProps,
|
|
||||||
positionProps,
|
|
||||||
focusProps,
|
|
||||||
otherProps,
|
|
||||||
props,
|
|
||||||
),
|
|
||||||
className: slots.base({class: clsx(baseStyles, props.className)}),
|
|
||||||
id: popoverId,
|
id: popoverId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getDialogProps: PropGetter = (props = {}) => ({
|
||||||
|
className: slots.base({class: clsx(baseStyles, props.className)}),
|
||||||
|
...mergeProps(focusProps, props),
|
||||||
|
style: {
|
||||||
|
// this prevent the dialog to have a default outline
|
||||||
|
outline: "none",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const getTriggerProps = useCallback<PropGetter>(
|
const getTriggerProps = useCallback<PropGetter>(
|
||||||
(props = {}, _ref: Ref<any> | null | undefined = null) => {
|
(props = {}, _ref: Ref<any> | null | undefined = null) => {
|
||||||
return {
|
return {
|
||||||
@ -218,9 +194,11 @@ export function usePopover(originalProps: UsePopoverProps) {
|
|||||||
onClose: state.close,
|
onClose: state.close,
|
||||||
disableAnimation: originalProps.disableAnimation ?? false,
|
disableAnimation: originalProps.disableAnimation ?? false,
|
||||||
motionProps,
|
motionProps,
|
||||||
|
focusProps,
|
||||||
getPopoverProps,
|
getPopoverProps,
|
||||||
getTriggerProps,
|
getTriggerProps,
|
||||||
getArrowProps,
|
getArrowProps,
|
||||||
|
getDialogProps,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {ComponentStory, ComponentMeta} from "@storybook/react";
|
import {ComponentStory, ComponentMeta} from "@storybook/react";
|
||||||
import {popover} from "@nextui-org/theme";
|
import {popover, ButtonVariantProps} from "@nextui-org/theme";
|
||||||
import {Button} from "@nextui-org/button";
|
import {Button} from "@nextui-org/button";
|
||||||
|
|
||||||
import {Popover, PopoverTrigger, PopoverContent, PopoverProps} from "../src";
|
import {Popover, PopoverTrigger, PopoverContent, PopoverProps} from "../src";
|
||||||
@ -56,11 +56,6 @@ export default {
|
|||||||
type: "boolean",
|
type: "boolean",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
isDisabled: {
|
|
||||||
control: {
|
|
||||||
type: "boolean",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
showArrow: {
|
showArrow: {
|
||||||
control: {
|
control: {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
@ -91,35 +86,295 @@ const defaultProps = {
|
|||||||
placement: "top",
|
placement: "top",
|
||||||
offset: 7,
|
offset: 7,
|
||||||
defaultOpen: false,
|
defaultOpen: false,
|
||||||
isDisabled: false,
|
|
||||||
disableAnimation: false,
|
disableAnimation: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<PopoverContent>
|
||||||
|
<div className="px-1 py-2">
|
||||||
|
<div className="text-sm font-bold">Popover Content</div>
|
||||||
|
<div className="text-xs">This is a content of the popover</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
);
|
||||||
|
|
||||||
const Template: ComponentStory<typeof Popover> = (args: PopoverProps) => {
|
const Template: ComponentStory<typeof Popover> = (args: PopoverProps) => {
|
||||||
return (
|
return (
|
||||||
<Popover {...args}>
|
<Popover {...args}>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button>Open popover</Button>
|
<Button>Open popover</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent>
|
{content}
|
||||||
<div className="px-1 py-2">
|
|
||||||
<div className="text-sm font-bold">Popover Content</div>
|
|
||||||
<div className="text-xs">This is a content of the popover</div>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const OpenChangeTemplate: ComponentStory<typeof Popover> = (args: PopoverProps) => {
|
||||||
|
const [isOpen, setIsOpen] = React.useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Popover {...args} onOpenChange={(open) => setIsOpen(open)}>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button>Open popover</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent>
|
||||||
|
<div className="px-1 py-2">
|
||||||
|
<div className="text-sm font-bold">Popover Content</div>
|
||||||
|
<div className="text-xs">This is a content of the popover</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<p className="text-sm">isOpen: {isOpen ? "true" : "false"}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const VariantsTemplate: ComponentStory<typeof Popover> = (args: PopoverProps) => {
|
||||||
|
const buttonColor = args.color as ButtonVariantProps["color"];
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<PopoverContent>
|
||||||
|
<div className="px-1 py-2">
|
||||||
|
<div className="text-sm font-bold">Popover Content</div>
|
||||||
|
<div className="text-xs">This is a content of the popover</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Popover {...args} variant="solid">
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button color={buttonColor}>Solid</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
{content}
|
||||||
|
</Popover>
|
||||||
|
<Popover {...args} variant="bordered">
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button color={buttonColor} variant="bordered">
|
||||||
|
Bordered
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
{content}
|
||||||
|
</Popover>
|
||||||
|
<Popover {...args} variant="light">
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button color={buttonColor} variant="light">
|
||||||
|
Light
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
{content}
|
||||||
|
</Popover>
|
||||||
|
<Popover {...args} variant="flat">
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button color={buttonColor} variant="flat">
|
||||||
|
Flat
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
{content}
|
||||||
|
</Popover>
|
||||||
|
<Popover {...args} variant="faded">
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button color={buttonColor} variant="faded">
|
||||||
|
Faded
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
{content}
|
||||||
|
</Popover>
|
||||||
|
<Popover {...args} variant="shadow">
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button color={buttonColor} variant="shadow">
|
||||||
|
Shadow
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
{content}
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PlacementsTemplate: ComponentStory<typeof Popover> = (args: PopoverProps) => {
|
||||||
|
const buttonColor = args.color as ButtonVariantProps["color"];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="inline-grid grid-cols-3 gap-4">
|
||||||
|
<Popover {...args} placement="top-start">
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button color={buttonColor} variant="flat">
|
||||||
|
Top Start
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
{content}
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<Popover {...args}>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button color={buttonColor} variant="flat">
|
||||||
|
Top
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
{content}
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<Popover {...args} placement="top-end">
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button color={buttonColor} variant="flat">
|
||||||
|
Top End
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
{content}
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<Popover {...args} placement="bottom-start">
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button color={buttonColor} variant="flat">
|
||||||
|
Bottom Start
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
{content}
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<Popover {...args} placement="bottom">
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button color={buttonColor} variant="flat">
|
||||||
|
Bottom
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
{content}
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<Popover {...args} placement="bottom-end">
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button color={buttonColor} variant="flat">
|
||||||
|
Bottom End
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
{content}
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<Popover {...args} placement="right-start">
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button color={buttonColor} variant="flat">
|
||||||
|
Right Start
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
{content}
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<Popover {...args} placement="right">
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button color={buttonColor} variant="flat">
|
||||||
|
Right
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
{content}
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<Popover {...args} placement="right-end">
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button color={buttonColor} variant="flat">
|
||||||
|
Right End
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
{content}
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<Popover {...args} placement="left-start">
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button color={buttonColor} variant="flat">
|
||||||
|
Left Start
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
{content}
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<Popover {...args} placement="left">
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button color={buttonColor} variant="flat">
|
||||||
|
Left
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
{content}
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<Popover {...args} placement="left-end">
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button color={buttonColor} variant="flat">
|
||||||
|
Left End
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
{content}
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const OffsetTemplate: ComponentStory<typeof Popover> = (args: PopoverProps) => (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Popover {...args}>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button color="warning" variant="faded">
|
||||||
|
Default offset (7)
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
{content}
|
||||||
|
</Popover>
|
||||||
|
<Popover {...args} offset={15}>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button color="warning" variant="faded">
|
||||||
|
15 offset
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
{content}
|
||||||
|
</Popover>
|
||||||
|
<Popover {...args} offset={-7}>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button color="warning" variant="faded">
|
||||||
|
-7 offset
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
{content}
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export const Default = Template.bind({});
|
export const Default = Template.bind({});
|
||||||
Default.args = {
|
Default.args = {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
showArrow: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DisableAnimation = Template.bind({});
|
export const DisableAnimation = Template.bind({});
|
||||||
DisableAnimation.args = {
|
DisableAnimation.args = {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
showArrow: true,
|
|
||||||
disableAnimation: true,
|
disableAnimation: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const WithArrow = Template.bind({});
|
||||||
|
WithArrow.args = {
|
||||||
|
...defaultProps,
|
||||||
|
showArrow: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OpenChange = OpenChangeTemplate.bind({});
|
||||||
|
OpenChange.args = {
|
||||||
|
...defaultProps,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Variants = VariantsTemplate.bind({});
|
||||||
|
Variants.args = {
|
||||||
|
...defaultProps,
|
||||||
|
color: "primary",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Placements = PlacementsTemplate.bind({});
|
||||||
|
Placements.args = {
|
||||||
|
...defaultProps,
|
||||||
|
color: "secondary",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithOffset = OffsetTemplate.bind({});
|
||||||
|
WithOffset.args = {
|
||||||
|
...defaultProps,
|
||||||
|
color: "warning",
|
||||||
|
};
|
||||||
|
|||||||
@ -84,7 +84,6 @@ const Tooltip = forwardRef<TooltipProps, "div">((props, ref) => {
|
|||||||
{content}
|
{content}
|
||||||
{arrowContent}
|
{arrowContent}
|
||||||
</Component>
|
</Component>
|
||||||
;
|
|
||||||
</OverlayContainer>
|
</OverlayContainer>
|
||||||
) : (
|
) : (
|
||||||
<AnimatePresence initial={false}>
|
<AnimatePresence initial={false}>
|
||||||
|
|||||||
@ -303,6 +303,12 @@ Default.args = {
|
|||||||
...defaultProps,
|
...defaultProps,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DisableAnimation = Template.bind({});
|
||||||
|
DisableAnimation.args = {
|
||||||
|
...defaultProps,
|
||||||
|
disableAnimation: true,
|
||||||
|
};
|
||||||
|
|
||||||
export const WithArrow = Template.bind({});
|
export const WithArrow = Template.bind({});
|
||||||
WithArrow.args = {
|
WithArrow.args = {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user