mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
feat(modal): initial structure created
This commit is contained in:
parent
d3bb78d9c1
commit
6bf21ad1ac
@ -51,7 +51,7 @@ export interface Props<T extends object = {}>
|
||||
*/
|
||||
startContent?: ReactNode;
|
||||
/**
|
||||
* The properties passed to the underlying `Collapse` component.
|
||||
* The props to modify the framer motion animation. Use the `variants` API to create your own animation.
|
||||
*/
|
||||
motionProps?: CollapseTransitionProps;
|
||||
/**
|
||||
|
||||
24
packages/components/modal/README.md
Normal file
24
packages/components/modal/README.md
Normal file
@ -0,0 +1,24 @@
|
||||
# @nextui-org/modal
|
||||
|
||||
A Quick description of the component
|
||||
|
||||
> This is an internal utility, not intended for public usage.
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
yarn add @nextui-org/modal
|
||||
# or
|
||||
npm i @nextui-org/modal
|
||||
```
|
||||
|
||||
## Contribution
|
||||
|
||||
Yes please! See the
|
||||
[contributing guidelines](https://github.com/nextui-org/nextui/blob/master/CONTRIBUTING.md)
|
||||
for details.
|
||||
|
||||
## Licence
|
||||
|
||||
This project is licensed under the terms of the
|
||||
[MIT license](https://github.com/nextui-org/nextui/blob/master/LICENSE).
|
||||
19
packages/components/modal/__tests__/modal.test.tsx
Normal file
19
packages/components/modal/__tests__/modal.test.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import * as React from "react";
|
||||
import {render} from "@testing-library/react";
|
||||
|
||||
import {Modal} from "../src";
|
||||
|
||||
describe("Modal", () => {
|
||||
it("should render correctly", () => {
|
||||
const wrapper = render(<Modal />);
|
||||
|
||||
expect(() => wrapper.unmount()).not.toThrow();
|
||||
});
|
||||
|
||||
it("ref should be forwarded", () => {
|
||||
const ref = React.createRef<HTMLDivElement>();
|
||||
|
||||
render(<Modal ref={ref} />);
|
||||
expect(ref.current).not.toBeNull();
|
||||
});
|
||||
});
|
||||
70
packages/components/modal/package.json
Normal file
70
packages/components/modal/package.json
Normal file
@ -0,0 +1,70 @@
|
||||
{
|
||||
"name": "@nextui-org/modal",
|
||||
"version": "2.0.0-beta.1",
|
||||
"description": "Displays a dialog with a custom content that requires attention or provides additional information.",
|
||||
"keywords": [
|
||||
"modal"
|
||||
],
|
||||
"author": "Junior Garcia <jrgarciadev@gmail.com>",
|
||||
"homepage": "https://nextui.org",
|
||||
"license": "MIT",
|
||||
"main": "src/index.ts",
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/nextui-org/nextui.git",
|
||||
"directory": "packages/components/modal"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/nextui-org/nextui/issues"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup src --dts",
|
||||
"build:fast": "tsup src",
|
||||
"dev": "yarn build:fast -- --watch",
|
||||
"clean": "rimraf dist .turbo",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"prepack": "clean-package",
|
||||
"postpack": "clean-package restore"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nextui-org/system": "workspace:*",
|
||||
"@nextui-org/theme": "workspace:*",
|
||||
"@nextui-org/button": "workspace:*",
|
||||
"@nextui-org/framer-transitions": "workspace:*",
|
||||
"@nextui-org/use-aria-button": "workspace:*",
|
||||
"@nextui-org/shared-utils": "workspace:*",
|
||||
"@nextui-org/dom-utils": "workspace:*",
|
||||
"@react-aria/dialog": "^3.5.1",
|
||||
"@react-aria/interactions": "^3.15.0",
|
||||
"@react-aria/overlays": "^3.14.0",
|
||||
"@react-aria/utils": "^3.16.0",
|
||||
"@react-stately/overlays": "^3.5.1",
|
||||
"@react-aria/focus": "^3.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nextui-org/input": "workspace:*",
|
||||
"framer-motion": "^10.11.2",
|
||||
"@react-types/overlays": "^3.7.1",
|
||||
"clean-package": "2.2.0",
|
||||
"react": "^18.0.0"
|
||||
},
|
||||
"clean-package": "../../../clean-package.config.json",
|
||||
"tsup": {
|
||||
"clean": true,
|
||||
"target": "es2019",
|
||||
"format": [
|
||||
"cjs",
|
||||
"esm"
|
||||
]
|
||||
}
|
||||
}
|
||||
17
packages/components/modal/src/index.ts
Normal file
17
packages/components/modal/src/index.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import Modal from "./modal";
|
||||
import ModalTrigger from "./modal-trigger";
|
||||
import ModalContent from "./modal-content";
|
||||
|
||||
// export types
|
||||
export type {ModalProps} from "./modal";
|
||||
export type {ModalTriggerProps} from "./modal-trigger";
|
||||
export type {ModalContentProps} from "./modal-content";
|
||||
|
||||
// export hooks
|
||||
export {useModal} from "./use-modal";
|
||||
|
||||
// export context
|
||||
export * from "./modal-context";
|
||||
|
||||
// export components
|
||||
export {Modal, ModalTrigger, ModalContent};
|
||||
100
packages/components/modal/src/modal-content.tsx
Normal file
100
packages/components/modal/src/modal-content.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
import type {AriaDialogProps} from "@react-aria/dialog";
|
||||
import type {HTMLMotionProps} from "framer-motion";
|
||||
|
||||
import {DOMAttributes, ReactNode, useMemo} from "react";
|
||||
import {forwardRef} from "@nextui-org/system";
|
||||
import {DismissButton} from "@react-aria/overlays";
|
||||
import {FocusScope} from "@react-aria/focus";
|
||||
import {TRANSITION_VARIANTS} from "@nextui-org/framer-transitions";
|
||||
import {motion} from "framer-motion";
|
||||
import {useDialog} from "@react-aria/dialog";
|
||||
import {mergeProps} from "@react-aria/utils";
|
||||
import {FocusableElement} from "@react-types/shared";
|
||||
|
||||
import {useModalContext} from "./modal-context";
|
||||
|
||||
export interface ModalContentProps extends AriaDialogProps {
|
||||
children: ReactNode | ((titleProps: DOMAttributes<FocusableElement>) => ReactNode);
|
||||
}
|
||||
|
||||
const ModalContent = forwardRef<ModalContentProps, "section">((props, _) => {
|
||||
const {as, children, ...otherProps} = props;
|
||||
|
||||
const {
|
||||
Component: DialogComponent,
|
||||
dialogRef,
|
||||
slots,
|
||||
classNames,
|
||||
motionProps,
|
||||
backdropVariant,
|
||||
disableAnimation,
|
||||
getDialogProps,
|
||||
getBackdropProps,
|
||||
onClose,
|
||||
} = useModalContext();
|
||||
|
||||
const Component = as || DialogComponent || "section";
|
||||
|
||||
const {dialogProps, titleProps} = useDialog(
|
||||
{
|
||||
role: "dialog",
|
||||
},
|
||||
dialogRef,
|
||||
);
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<DismissButton onDismiss={onClose} />
|
||||
<Component {...getDialogProps(mergeProps(dialogProps, otherProps))}>
|
||||
<FocusScope contain restoreFocus>
|
||||
{typeof children === "function" ? children(titleProps) : children}
|
||||
</FocusScope>
|
||||
</Component>
|
||||
<DismissButton onDismiss={onClose} />
|
||||
</>
|
||||
);
|
||||
|
||||
const backdrop = useMemo(() => {
|
||||
if (backdropVariant === "transparent") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (disableAnimation) {
|
||||
return <div {...getBackdropProps()} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
animate="enter"
|
||||
exit="exit"
|
||||
initial="exit"
|
||||
variants={TRANSITION_VARIANTS.fade}
|
||||
{...(getBackdropProps() as HTMLMotionProps<"div">)}
|
||||
/>
|
||||
);
|
||||
}, [backdropVariant, disableAnimation, getBackdropProps]);
|
||||
|
||||
return (
|
||||
<div tabIndex={-1}>
|
||||
{backdrop}
|
||||
{disableAnimation ? (
|
||||
<div className={slots.wrapper({class: classNames?.wrapper})}>content</div>
|
||||
) : (
|
||||
<motion.div
|
||||
animate="enter"
|
||||
className={slots.wrapper({class: classNames?.wrapper})}
|
||||
exit="exit"
|
||||
initial="exit"
|
||||
variants={TRANSITION_VARIANTS.scaleSpring}
|
||||
{...motionProps}
|
||||
>
|
||||
{content}
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
ModalContent.displayName = "NextUI.ModalContent";
|
||||
|
||||
export default ModalContent;
|
||||
9
packages/components/modal/src/modal-context.ts
Normal file
9
packages/components/modal/src/modal-context.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import {createContext} from "@nextui-org/shared-utils";
|
||||
|
||||
import {UseModalReturn} from "./use-modal";
|
||||
|
||||
export const [ModalProvider, useModalContext] = createContext<UseModalReturn>({
|
||||
name: "ModalContext",
|
||||
errorMessage:
|
||||
"useModalContext: `context` is undefined. Seems you forgot to wrap all popover components within `<Modal />`",
|
||||
});
|
||||
50
packages/components/modal/src/modal-trigger.tsx
Normal file
50
packages/components/modal/src/modal-trigger.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import {forwardRef} from "@nextui-org/system";
|
||||
import React, {Children, cloneElement, useMemo} from "react";
|
||||
import {pickChildren} from "@nextui-org/shared-utils";
|
||||
import {useAriaButton} from "@nextui-org/use-aria-button";
|
||||
import {Button} from "@nextui-org/button";
|
||||
import {mergeProps} from "@react-aria/utils";
|
||||
|
||||
import {useModalContext} from "./modal-context";
|
||||
|
||||
export interface ModalTriggerProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* ModalTrigger opens the popover's content. It must be an interactive element
|
||||
* such as `button` or `a`.
|
||||
*/
|
||||
const ModalTrigger = forwardRef<ModalTriggerProps, "button">((props, _) => {
|
||||
const {triggerRef, getTriggerProps} = useModalContext();
|
||||
|
||||
const {children, ...otherProps} = props;
|
||||
|
||||
// force a single child
|
||||
const child = useMemo<any>(() => {
|
||||
if (typeof children === "string") return <p>{children}</p>;
|
||||
|
||||
return Children.only(children) as React.ReactElement & {
|
||||
ref?: React.Ref<any>;
|
||||
};
|
||||
}, [children]);
|
||||
|
||||
const {onPress, ...rest} = useMemo(() => {
|
||||
return getTriggerProps(mergeProps(child.props, otherProps), child.ref);
|
||||
}, [getTriggerProps, child.props, otherProps, child.ref]);
|
||||
|
||||
// validates if contains a NextUI Button as a child
|
||||
const [, triggerChildren] = pickChildren(children, Button);
|
||||
|
||||
const {buttonProps} = useAriaButton({onPress}, triggerRef);
|
||||
|
||||
const hasNextUIButton = useMemo<boolean>(() => {
|
||||
return triggerChildren?.[0] !== undefined;
|
||||
}, [triggerChildren]);
|
||||
|
||||
return cloneElement(child, mergeProps(rest, hasNextUIButton ? {onPress} : buttonProps));
|
||||
});
|
||||
|
||||
ModalTrigger.displayName = "NextUI.ModalTrigger";
|
||||
|
||||
export default ModalTrigger;
|
||||
39
packages/components/modal/src/modal.tsx
Normal file
39
packages/components/modal/src/modal.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import {forwardRef} from "@nextui-org/system";
|
||||
import {Children, ReactNode} from "react";
|
||||
import {AnimatePresence} from "framer-motion";
|
||||
import {Overlay} from "@react-aria/overlays";
|
||||
|
||||
import {UseModalProps, useModal} from "./use-modal";
|
||||
import {ModalProvider} from "./modal-context";
|
||||
|
||||
export interface ModalProps extends Omit<UseModalProps, "ref"> {
|
||||
/**
|
||||
* The content of the popover. It is usually the `ModalTrigger`,
|
||||
* and `ModalContent`
|
||||
*/
|
||||
children: ReactNode[];
|
||||
}
|
||||
|
||||
const Modal = forwardRef<ModalProps, "div">((props, ref) => {
|
||||
const {children, ...otherProps} = props;
|
||||
const context = useModal({ref, ...otherProps});
|
||||
|
||||
const [trigger, content] = Children.toArray(children);
|
||||
|
||||
const overlay = <Overlay>{content}</Overlay>;
|
||||
|
||||
return (
|
||||
<ModalProvider value={context}>
|
||||
{trigger}
|
||||
{context.disableAnimation && context.isOpen ? (
|
||||
overlay
|
||||
) : (
|
||||
<AnimatePresence initial={false}>{context.isOpen ? overlay : null}</AnimatePresence>
|
||||
)}
|
||||
</ModalProvider>
|
||||
);
|
||||
});
|
||||
|
||||
Modal.displayName = "NextUI.Modal";
|
||||
|
||||
export default Modal;
|
||||
176
packages/components/modal/src/use-modal.ts
Normal file
176
packages/components/modal/src/use-modal.ts
Normal file
@ -0,0 +1,176 @@
|
||||
import type {ModalVariantProps, SlotsToClasses, ModalSlots} from "@nextui-org/theme";
|
||||
import type {HTMLMotionProps} from "framer-motion";
|
||||
|
||||
import {AriaModalOverlayProps, useModalOverlay} from "@react-aria/overlays";
|
||||
import {
|
||||
RefObject,
|
||||
Ref,
|
||||
useCallback,
|
||||
useId,
|
||||
useRef,
|
||||
useState,
|
||||
useMemo,
|
||||
useImperativeHandle,
|
||||
} from "react";
|
||||
import {HTMLNextUIProps, mapPropsVariants, PropGetter} from "@nextui-org/system";
|
||||
import {modal} from "@nextui-org/theme";
|
||||
import {clsx, ReactRef} from "@nextui-org/shared-utils";
|
||||
import {useOverlayTrigger} from "@react-aria/overlays";
|
||||
import {createDOMRef} from "@nextui-org/dom-utils";
|
||||
import {useOverlayTriggerState} from "@react-stately/overlays";
|
||||
import {OverlayTriggerProps} from "@react-stately/overlays";
|
||||
import {mergeRefs, mergeProps} from "@react-aria/utils";
|
||||
|
||||
interface Props extends HTMLNextUIProps<"div"> {
|
||||
/**
|
||||
* Ref to the DOM node.
|
||||
*/
|
||||
ref?: ReactRef<HTMLElement | null>;
|
||||
/**
|
||||
* The ref for the element which the overlay positions itself with respect to.
|
||||
*/
|
||||
triggerRef?: RefObject<HTMLElement>;
|
||||
/**
|
||||
* The props to modify the framer motion animation. Use the `variants` API to create your own animation.
|
||||
*/
|
||||
motionProps?: HTMLMotionProps<"div">;
|
||||
|
||||
/**
|
||||
* Whether the animation should be disabled.
|
||||
* @default false
|
||||
*/
|
||||
disableAnimation?: boolean;
|
||||
/**
|
||||
* Classname or List of classes to change the classNames of the element.
|
||||
* if `className` is passed, it will be added to the base slot.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* <Modal classNames={{
|
||||
* base:"base-classes",
|
||||
* header: "header-classes",
|
||||
* body: "body-classes",
|
||||
* footer: "footer-classes",
|
||||
* }} />
|
||||
* ```
|
||||
*/
|
||||
classNames?: SlotsToClasses<ModalSlots>;
|
||||
}
|
||||
|
||||
export type UseModalProps = Props & OverlayTriggerProps & AriaModalOverlayProps & ModalVariantProps;
|
||||
|
||||
export function useModal(originalProps: UseModalProps) {
|
||||
const [props, variantProps] = mapPropsVariants(originalProps, modal.variantKeys);
|
||||
|
||||
const {
|
||||
ref,
|
||||
as,
|
||||
className,
|
||||
classNames,
|
||||
triggerRef: triggerRefProp,
|
||||
disableAnimation = false,
|
||||
isOpen,
|
||||
defaultOpen,
|
||||
onOpenChange,
|
||||
motionProps,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const Component = as || "section";
|
||||
|
||||
const dialogRef = useRef<HTMLElement>(null);
|
||||
const domTriggerRef = useRef<HTMLElement>(null);
|
||||
|
||||
const [headerMounted, setHeaderMounted] = useState(false);
|
||||
const [bodyMounted, setBodyMounted] = useState(false);
|
||||
|
||||
const triggerRef = triggerRefProp || domTriggerRef;
|
||||
|
||||
const dialogId = useId();
|
||||
const headerId = useId();
|
||||
const bodyId = useId();
|
||||
|
||||
// Sync ref with popoverRef from passed ref.
|
||||
useImperativeHandle(ref, () =>
|
||||
// @ts-ignore
|
||||
createDOMRef(dialogRef),
|
||||
);
|
||||
|
||||
const state = useOverlayTriggerState({
|
||||
isOpen,
|
||||
defaultOpen,
|
||||
onOpenChange,
|
||||
});
|
||||
|
||||
const {triggerProps} = useOverlayTrigger({type: "dialog"}, state, triggerRef);
|
||||
|
||||
const {modalProps, underlayProps} = useModalOverlay(originalProps, state, dialogRef);
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
|
||||
const slots = useMemo(
|
||||
() =>
|
||||
modal({
|
||||
...variantProps,
|
||||
}),
|
||||
[...Object.values(variantProps)],
|
||||
);
|
||||
|
||||
const getDialogProps: PropGetter = useCallback(
|
||||
(props = {}, ref = null) => ({
|
||||
ref: mergeRefs(ref, dialogRef),
|
||||
...mergeProps(modalProps, otherProps, props),
|
||||
className: slots.base({class: clsx(baseStyles, props.className)}),
|
||||
id: dialogId,
|
||||
"aria-modal": true,
|
||||
"aria-labelledby": headerMounted ? headerId : undefined,
|
||||
"aria-describedby": bodyMounted ? bodyId : undefined,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const getTriggerProps = useCallback<PropGetter>(
|
||||
(props = {}, _ref: Ref<any> | null | undefined = null) => {
|
||||
return {
|
||||
...mergeProps(triggerProps, props),
|
||||
className: slots.trigger({class: clsx(classNames?.trigger, props.className)}),
|
||||
ref: mergeRefs(_ref, triggerRef),
|
||||
"aria-controls": dialogId,
|
||||
"aria-haspopup": "dialog",
|
||||
};
|
||||
},
|
||||
[isOpen, dialogId, state, triggerProps, triggerRef],
|
||||
);
|
||||
|
||||
const getBackdropProps = useCallback<PropGetter>(
|
||||
(props = {}) => ({
|
||||
className: slots.backdrop({class: classNames?.backdrop}),
|
||||
onClick: () => state.close(),
|
||||
...underlayProps,
|
||||
...props,
|
||||
}),
|
||||
[slots, classNames, underlayProps],
|
||||
);
|
||||
|
||||
return {
|
||||
Component,
|
||||
slots,
|
||||
dialogRef,
|
||||
headerId,
|
||||
bodyId,
|
||||
triggerRef,
|
||||
motionProps,
|
||||
classNames,
|
||||
backdropVariant: originalProps.backdropVariant ?? "opaque",
|
||||
isOpen: state.isOpen,
|
||||
onClose: state.close,
|
||||
disableAnimation,
|
||||
setBodyMounted,
|
||||
setHeaderMounted,
|
||||
getDialogProps,
|
||||
getTriggerProps,
|
||||
getBackdropProps,
|
||||
};
|
||||
}
|
||||
|
||||
export type UseModalReturn = ReturnType<typeof useModal>;
|
||||
73
packages/components/modal/stories/modal.stories.tsx
Normal file
73
packages/components/modal/stories/modal.stories.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import React from "react";
|
||||
import {ComponentStory, ComponentMeta} from "@storybook/react";
|
||||
import {modal} from "@nextui-org/theme";
|
||||
import {Button} from "@nextui-org/button";
|
||||
|
||||
import {Modal, ModalContent, ModalTrigger, ModalProps} from "../src";
|
||||
|
||||
export default {
|
||||
title: "Components/Modal",
|
||||
component: Modal,
|
||||
argTypes: {
|
||||
size: {
|
||||
control: {
|
||||
type: "select",
|
||||
options: ["xs", "sm", "md", "lg", "xl", "2xl", "3xl", "4xl", "5xl", "6xl", "full", "prose"],
|
||||
},
|
||||
},
|
||||
radius: {
|
||||
control: {
|
||||
type: "select",
|
||||
options: ["none", "base", "sm", "md", "lg", "xl"],
|
||||
},
|
||||
},
|
||||
backdropVariant: {
|
||||
control: {
|
||||
type: "select",
|
||||
options: ["transparent", "blur", "opaque"],
|
||||
},
|
||||
},
|
||||
disableAnimation: {
|
||||
control: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
children: {
|
||||
control: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="flex items-center justify-center w-screen h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
} as ComponentMeta<typeof Modal>;
|
||||
|
||||
const defaultProps = {
|
||||
...modal.defaultVariants,
|
||||
};
|
||||
|
||||
const Template: ComponentStory<typeof Modal> = (args: ModalProps) => {
|
||||
return (
|
||||
<Modal {...args}>
|
||||
<ModalTrigger>
|
||||
<Button disableAnimation={!!args.disableAnimation}>Open popover</Button>
|
||||
</ModalTrigger>
|
||||
<ModalContent>
|
||||
<div className="px-1 py-2">
|
||||
<div className="text-sm font-bold">Modal Content</div>
|
||||
<div className="text-xs">This is a content of the modal</div>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
...defaultProps,
|
||||
};
|
||||
10
packages/components/modal/tsconfig.json
Normal file
10
packages/components/modal/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"tailwind-variants": ["../../../node_modules/tailwind-variants"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "index.ts"]
|
||||
}
|
||||
@ -46,7 +46,6 @@
|
||||
"@nextui-org/shared-utils": "workspace:*",
|
||||
"@nextui-org/system": "workspace:*",
|
||||
"@nextui-org/theme": "workspace:*",
|
||||
"@react-aria/button": "^3.7.1",
|
||||
"@react-aria/dialog": "^3.5.1",
|
||||
"@react-aria/interactions": "^3.15.0",
|
||||
"@react-aria/overlays": "^3.14.0",
|
||||
|
||||
@ -35,7 +35,7 @@ const PopoverContent = forwardRef<PopoverContentProps, "section">((props, _) =>
|
||||
onClose,
|
||||
} = usePopoverContext();
|
||||
|
||||
const Component = as || OverlayComponent || "div";
|
||||
const Component = as || OverlayComponent || "section";
|
||||
|
||||
const dialogRef = useRef(null);
|
||||
const {dialogProps, titleProps} = useDialog(
|
||||
|
||||
@ -9,7 +9,7 @@ import {PopoverProvider} from "./popover-context";
|
||||
export interface PopoverProps extends Omit<UsePopoverProps, "ref"> {
|
||||
/**
|
||||
* The content of the popover. It is usually the `PopoverTrigger`,
|
||||
* and `Popover.Content`
|
||||
* and `PopoverContent`
|
||||
*/
|
||||
children: ReactNode[];
|
||||
}
|
||||
|
||||
@ -52,7 +52,7 @@ export interface Props extends HTMLNextUIProps<"div"> {
|
||||
*/
|
||||
triggerType?: "dialog" | "menu" | "listbox" | "tree" | "grid";
|
||||
/**
|
||||
* The properties passed to the underlying `Collapse` component.
|
||||
* The props to modify the framer motion animation. Use the `variants` API to create your own animation.
|
||||
*/
|
||||
motionProps?: HTMLMotionProps<"div">;
|
||||
/**
|
||||
|
||||
@ -48,7 +48,7 @@ interface Props extends HTMLNextUIProps<"div"> {
|
||||
*/
|
||||
trigger?: "focus";
|
||||
/**
|
||||
* The properties passed to the underlying `Collapse` component.
|
||||
* The props to modify the framer motion animation. Use the `variants` API to create your own animation.
|
||||
*/
|
||||
motionProps?: HTMLMotionProps<"div">;
|
||||
/**
|
||||
|
||||
@ -28,3 +28,4 @@ export * from "./dropdown-item";
|
||||
export * from "./dropdown-section";
|
||||
export * from "./dropdown-menu";
|
||||
export * from "./image";
|
||||
export * from "./modal";
|
||||
|
||||
131
packages/core/theme/src/components/modal.ts
Normal file
131
packages/core/theme/src/components/modal.ts
Normal file
@ -0,0 +1,131 @@
|
||||
import type {VariantProps} from "tailwind-variants";
|
||||
|
||||
import {tv} from "tailwind-variants";
|
||||
|
||||
// import {ringClasses} from "../utils";
|
||||
|
||||
/**
|
||||
* Card **Tailwind Variants** component
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* const {base, trigger, backdrop, header, body, footer} = modal({...})
|
||||
*
|
||||
* <div>
|
||||
* <button className={trigger()}>Open Modal</button>
|
||||
* <div className={backdrop()}/>
|
||||
* <div className={base()}>
|
||||
* <div className={header()}>Header</div>
|
||||
* <div className={body()}>Body</div>
|
||||
* <div className={footer()}>Footer</div>
|
||||
* </div>
|
||||
* </div>
|
||||
* ```
|
||||
*/
|
||||
const modal = tv({
|
||||
slots: {
|
||||
wrapper: [
|
||||
"flex",
|
||||
"w-screen",
|
||||
"h-screen",
|
||||
"fixed",
|
||||
"inset-0",
|
||||
"z-50",
|
||||
"overflow-x-auto",
|
||||
"overflow-y-hidden",
|
||||
"justify-center",
|
||||
"items-center",
|
||||
"outline-none",
|
||||
],
|
||||
base: [
|
||||
"relative",
|
||||
"bg-white",
|
||||
"z-50",
|
||||
"w-full",
|
||||
"shadow-lg",
|
||||
"box-border",
|
||||
"dark:bg-content1",
|
||||
"border border-neutral-100",
|
||||
],
|
||||
trigger: [],
|
||||
backdrop: ["hidden"],
|
||||
header: [],
|
||||
body: [],
|
||||
footer: [],
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
xs: {
|
||||
base: "max-w-xs",
|
||||
},
|
||||
sm: {
|
||||
base: "max-w-sm",
|
||||
},
|
||||
md: {
|
||||
base: "max-w-md",
|
||||
},
|
||||
lg: {
|
||||
base: "max-w-lg",
|
||||
},
|
||||
xl: {
|
||||
base: "max-w-xl",
|
||||
},
|
||||
"2xl": {
|
||||
base: "max-w-2xl",
|
||||
},
|
||||
"3xl": {
|
||||
base: "max-w-3xl",
|
||||
},
|
||||
"4xl": {
|
||||
base: "max-w-4xl",
|
||||
},
|
||||
"5xl": {
|
||||
base: "max-w-5xl",
|
||||
},
|
||||
full: {
|
||||
base: "max-w-full",
|
||||
},
|
||||
prose: {
|
||||
base: "max-w-prose",
|
||||
},
|
||||
},
|
||||
radius: {
|
||||
none: {base: "rounded-none"},
|
||||
base: {base: "rounded"},
|
||||
sm: {base: "rounded-sm"},
|
||||
md: {base: "rounded-md"},
|
||||
lg: {base: "rounded-lg"},
|
||||
xl: {base: "rounded-xl"},
|
||||
"2xl": {base: "rounded-2xl"},
|
||||
"3xl": {base: "rounded-3xl"},
|
||||
},
|
||||
backdropVariant: {
|
||||
transparent: {},
|
||||
opaque: {
|
||||
backdrop: "bg-black/30 backdrop-opacity-50",
|
||||
},
|
||||
blur: {
|
||||
backdrop: "backdrop-blur-sm backdrop-saturate-150 bg-black/20",
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "md",
|
||||
radius: "lg",
|
||||
backdropVariant: "opaque",
|
||||
},
|
||||
compoundVariants: [
|
||||
// backdropVariant (opaque/blur)
|
||||
{
|
||||
backdropVariant: ["opaque", "blur"],
|
||||
class: {
|
||||
backdrop: "block w-full h-full fixed inset-0 z-0",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export type ModalVariantProps = VariantProps<typeof modal>;
|
||||
export type ModalSlots = keyof ReturnType<typeof modal>;
|
||||
|
||||
export {modal};
|
||||
61
pnpm-lock.yaml
generated
61
pnpm-lock.yaml
generated
@ -1004,6 +1004,64 @@ importers:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0
|
||||
|
||||
packages/components/modal:
|
||||
dependencies:
|
||||
'@nextui-org/button':
|
||||
specifier: workspace:*
|
||||
version: link:../button
|
||||
'@nextui-org/dom-utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../utilities/dom-utils
|
||||
'@nextui-org/framer-transitions':
|
||||
specifier: workspace:*
|
||||
version: link:../../utilities/framer-transitions
|
||||
'@nextui-org/shared-utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../utilities/shared-utils
|
||||
'@nextui-org/system':
|
||||
specifier: workspace:*
|
||||
version: link:../../core/system
|
||||
'@nextui-org/theme':
|
||||
specifier: workspace:*
|
||||
version: link:../../core/theme
|
||||
'@nextui-org/use-aria-button':
|
||||
specifier: workspace:*
|
||||
version: link:../../hooks/use-aria-button
|
||||
'@react-aria/dialog':
|
||||
specifier: ^3.5.1
|
||||
version: 3.5.1(react-dom@18.2.0)(react@18.2.0)
|
||||
'@react-aria/focus':
|
||||
specifier: ^3.12.0
|
||||
version: 3.12.0(react@18.2.0)
|
||||
'@react-aria/interactions':
|
||||
specifier: ^3.15.0
|
||||
version: 3.15.0(react@18.2.0)
|
||||
'@react-aria/overlays':
|
||||
specifier: ^3.14.0
|
||||
version: 3.14.0(react-dom@18.2.0)(react@18.2.0)
|
||||
'@react-aria/utils':
|
||||
specifier: ^3.16.0
|
||||
version: 3.16.0(react@18.2.0)
|
||||
'@react-stately/overlays':
|
||||
specifier: ^3.5.1
|
||||
version: 3.5.1(react@18.2.0)
|
||||
devDependencies:
|
||||
'@nextui-org/input':
|
||||
specifier: workspace:*
|
||||
version: link:../input
|
||||
'@react-types/overlays':
|
||||
specifier: ^3.7.1
|
||||
version: 3.7.1(react@18.2.0)
|
||||
clean-package:
|
||||
specifier: 2.2.0
|
||||
version: 2.2.0
|
||||
framer-motion:
|
||||
specifier: ^10.11.2
|
||||
version: 10.11.2(react-dom@18.2.0)(react@18.2.0)
|
||||
react:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0
|
||||
|
||||
packages/components/pagination:
|
||||
dependencies:
|
||||
'@nextui-org/dom-utils':
|
||||
@ -1067,9 +1125,6 @@ importers:
|
||||
'@nextui-org/use-aria-button':
|
||||
specifier: workspace:*
|
||||
version: link:../../hooks/use-aria-button
|
||||
'@react-aria/button':
|
||||
specifier: ^3.7.1
|
||||
version: 3.7.1(react@18.2.0)
|
||||
'@react-aria/dialog':
|
||||
specifier: ^3.5.1
|
||||
version: 3.5.1(react-dom@18.2.0)(react@18.2.0)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user