mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
feat(modal): initial tests added
This commit is contained in:
parent
93e7bd5105
commit
0bbdc4df8b
@ -1,19 +1,116 @@
|
||||
import * as React from "react";
|
||||
import {render} from "@testing-library/react";
|
||||
import {act, render, fireEvent} from "@testing-library/react";
|
||||
|
||||
import {Modal} from "../src";
|
||||
import {Modal, ModalTrigger, ModalContent, ModalBody, ModalHeader, ModalFooter} from "../src";
|
||||
|
||||
describe("Modal", () => {
|
||||
it("should render correctly", () => {
|
||||
const wrapper = render(<Modal />);
|
||||
const wrapper = render(
|
||||
<Modal>
|
||||
<ModalTrigger>
|
||||
<button>Open Modal</button>
|
||||
</ModalTrigger>
|
||||
<ModalContent>
|
||||
<ModalHeader>Modal header</ModalHeader>
|
||||
<ModalBody>Modal body</ModalBody>
|
||||
<ModalFooter>Modal footer</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>,
|
||||
);
|
||||
|
||||
expect(() => wrapper.unmount()).not.toThrow();
|
||||
});
|
||||
|
||||
it("ref should be forwarded", () => {
|
||||
const ref = React.createRef<HTMLDivElement>();
|
||||
const ref = React.createRef<HTMLElement>();
|
||||
|
||||
render(<Modal ref={ref} />);
|
||||
render(
|
||||
<Modal ref={ref}>
|
||||
<ModalTrigger>
|
||||
<button>Open Modal</button>
|
||||
</ModalTrigger>
|
||||
<ModalContent>
|
||||
<ModalHeader>Modal header</ModalHeader>
|
||||
<ModalBody>Modal body</ModalBody>
|
||||
<ModalFooter>Modal footer</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>,
|
||||
);
|
||||
expect(ref.current).not.toBeNull();
|
||||
});
|
||||
|
||||
test("should have the proper 'aria' attributes", () => {
|
||||
const {getByRole, getByText} = render(
|
||||
<Modal isOpen>
|
||||
<ModalTrigger>
|
||||
<button>Open Modal</button>
|
||||
</ModalTrigger>
|
||||
<ModalContent>
|
||||
<ModalHeader>Modal header</ModalHeader>
|
||||
<ModalBody>Modal body</ModalBody>
|
||||
<ModalFooter>Modal footer</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>,
|
||||
);
|
||||
|
||||
const modal = getByRole("dialog");
|
||||
|
||||
expect(modal).toHaveAttribute("aria-modal", "true");
|
||||
expect(modal).toHaveAttribute("role", "dialog");
|
||||
|
||||
const modalHeader = getByText("Modal header");
|
||||
|
||||
expect(modal).toHaveAttribute("aria-labelledby", modalHeader.id);
|
||||
|
||||
const modalBody = getByText("Modal body");
|
||||
|
||||
expect(modal).toHaveAttribute("aria-describedby", modalBody.id);
|
||||
});
|
||||
|
||||
test("should fire 'onOpenChange' callback when close button is clicked", () => {
|
||||
const onClose = jest.fn();
|
||||
|
||||
const {getByLabelText} = render(
|
||||
<Modal isOpen onClose={onClose}>
|
||||
<ModalTrigger>
|
||||
<button>Open Modal</button>
|
||||
</ModalTrigger>
|
||||
<ModalContent>
|
||||
<ModalHeader>Modal header</ModalHeader>
|
||||
<ModalBody>Modal body</ModalBody>
|
||||
<ModalFooter>Modal footer</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>,
|
||||
);
|
||||
|
||||
const closeButton = getByLabelText("Close");
|
||||
|
||||
act(() => {
|
||||
closeButton.click();
|
||||
});
|
||||
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should hide the modal when pressing the escape key", () => {
|
||||
const onClose = jest.fn();
|
||||
|
||||
const wrapper = render(
|
||||
<Modal isOpen onClose={onClose}>
|
||||
<ModalTrigger>
|
||||
<button>Open Modal</button>
|
||||
</ModalTrigger>
|
||||
<ModalContent>
|
||||
<ModalHeader>Modal header</ModalHeader>
|
||||
<ModalBody>Modal body</ModalBody>
|
||||
<ModalFooter>Modal footer</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>,
|
||||
);
|
||||
|
||||
const modal = wrapper.getByRole("dialog");
|
||||
|
||||
fireEvent.keyDown(modal, {key: "Escape"});
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -14,7 +14,7 @@ export interface ModalProps extends Omit<UseModalProps, "ref"> {
|
||||
children: ReactNode[];
|
||||
}
|
||||
|
||||
const Modal = forwardRef<ModalProps, "div">((props, ref) => {
|
||||
const Modal = forwardRef<ModalProps, "section">((props, ref) => {
|
||||
const {children, ...otherProps} = props;
|
||||
const context = useModal({ref, ...otherProps});
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ import {useOverlayTriggerState} from "@react-stately/overlays";
|
||||
import {OverlayTriggerProps} from "@react-stately/overlays";
|
||||
import {mergeRefs, mergeProps} from "@react-aria/utils";
|
||||
|
||||
interface Props extends HTMLNextUIProps<"div"> {
|
||||
interface Props extends HTMLNextUIProps<"section"> {
|
||||
/**
|
||||
* Ref to the DOM node.
|
||||
*/
|
||||
@ -35,7 +35,7 @@ interface Props extends HTMLNextUIProps<"div"> {
|
||||
/**
|
||||
* The props to modify the framer motion animation. Use the `variants` API to create your own animation.
|
||||
*/
|
||||
motionProps?: HTMLMotionProps<"div">;
|
||||
motionProps?: HTMLMotionProps<"section">;
|
||||
/**
|
||||
* Determines if the modal should have a close button in the top right corner.
|
||||
* @default true
|
||||
@ -46,6 +46,10 @@ interface Props extends HTMLNextUIProps<"div"> {
|
||||
* @default false
|
||||
*/
|
||||
disableAnimation?: boolean;
|
||||
/**
|
||||
* Callback fired when the modal is closed.
|
||||
*/
|
||||
onClose?: () => void;
|
||||
/**
|
||||
* Classname or List of classes to change the classNames of the element.
|
||||
* if `className` is passed, it will be added to the base slot.
|
||||
@ -85,6 +89,7 @@ export function useModal(originalProps: UseModalProps) {
|
||||
isDismissable = true,
|
||||
showCloseButton = true,
|
||||
isKeyboardDismissDisabled = false,
|
||||
onClose,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
@ -112,7 +117,12 @@ export function useModal(originalProps: UseModalProps) {
|
||||
const state = useOverlayTriggerState({
|
||||
isOpen,
|
||||
defaultOpen,
|
||||
onOpenChange,
|
||||
onOpenChange: (isOpen) => {
|
||||
onOpenChange?.(isOpen);
|
||||
if (!isOpen) {
|
||||
onClose?.();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const {triggerProps} = useOverlayTrigger({type: "dialog"}, state, triggerRef);
|
||||
@ -175,6 +185,7 @@ export function useModal(originalProps: UseModalProps) {
|
||||
return {
|
||||
role: "button",
|
||||
tabIndex: 0,
|
||||
"aria-label": "Close",
|
||||
className: slots.closeButton({class: classNames?.closeButton}),
|
||||
...mergeProps(closeButtonProps, closeButtonFocusProps),
|
||||
};
|
||||
|
||||
@ -26,7 +26,7 @@ export default {
|
||||
size: {
|
||||
control: {
|
||||
type: "select",
|
||||
options: ["xs", "sm", "md", "lg", "xl", "2xl", "3xl", "4xl", "5xl", "6xl", "full", "prose"],
|
||||
options: ["xs", "sm", "md", "lg", "xl", "2xl", "3xl", "4xl", "5xl", "6xl", "full"],
|
||||
},
|
||||
},
|
||||
radius: {
|
||||
@ -179,11 +179,50 @@ const OutsideScrollTemplate: ComponentStory<typeof Modal> = (args: ModalProps) =
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const OpenChangeTemplate: ComponentStory<typeof Modal> = (args: ModalProps) => {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Modal {...args} onOpenChange={(open) => setIsOpen(open)}>
|
||||
<ModalTrigger>
|
||||
<Button disableAnimation={!!args.disableAnimation}>Open Modal</Button>
|
||||
</ModalTrigger>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader>Modal Title</ModalHeader>
|
||||
<ModalBody>
|
||||
<Lorem size={5} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button onPress={onClose}>Close</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<p className="text-sm">isOpen: {isOpen ? "true" : "false"}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
...defaultProps,
|
||||
};
|
||||
|
||||
export const DefaultOpen = Template.bind({});
|
||||
DefaultOpen.args = {
|
||||
...defaultProps,
|
||||
defaultOpen: true,
|
||||
};
|
||||
|
||||
export const OpenChange = OpenChangeTemplate.bind({});
|
||||
OpenChange.args = {
|
||||
...defaultProps,
|
||||
};
|
||||
|
||||
export const InsideScroll = InsideScrollTemplate.bind({});
|
||||
InsideScroll.args = {
|
||||
...defaultProps,
|
||||
|
||||
@ -69,6 +69,10 @@ export interface Props extends HTMLNextUIProps<"div"> {
|
||||
* ```
|
||||
*/
|
||||
classNames?: SlotsToClasses<PopoverSlots>;
|
||||
/**
|
||||
* Callback fired when the popover is closed.
|
||||
*/
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export type UsePopoverProps = Props &
|
||||
@ -100,6 +104,7 @@ export function usePopover(originalProps: UsePopoverProps) {
|
||||
motionProps,
|
||||
className,
|
||||
classNames,
|
||||
onClose,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
@ -122,7 +127,12 @@ export function usePopover(originalProps: UsePopoverProps) {
|
||||
const innerState = useOverlayTriggerState({
|
||||
isOpen,
|
||||
defaultOpen,
|
||||
onOpenChange,
|
||||
onOpenChange: (isOpen) => {
|
||||
onOpenChange?.(isOpen);
|
||||
if (!isOpen) {
|
||||
onClose?.();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const state = stateProp || innerState;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user