mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
feat(modal): implementation done
This commit is contained in:
parent
2da9d4a8c9
commit
93e7bd5105
@ -43,6 +43,7 @@
|
||||
"@nextui-org/framer-transitions": "workspace:*",
|
||||
"@nextui-org/use-aria-button": "workspace:*",
|
||||
"@nextui-org/shared-utils": "workspace:*",
|
||||
"@nextui-org/shared-icons": "workspace:*",
|
||||
"@nextui-org/dom-utils": "workspace:*",
|
||||
"@react-aria/dialog": "^3.5.1",
|
||||
"@react-aria/interactions": "^3.15.0",
|
||||
@ -53,6 +54,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nextui-org/input": "workspace:*",
|
||||
"@nextui-org/checkbox": "workspace:*",
|
||||
"react-lorem-component": "0.13.0",
|
||||
"framer-motion": "^10.11.2",
|
||||
"@react-types/overlays": "^3.7.1",
|
||||
"clean-package": "2.2.0",
|
||||
|
||||
@ -1,11 +1,17 @@
|
||||
import Modal from "./modal";
|
||||
import ModalTrigger from "./modal-trigger";
|
||||
import ModalContent from "./modal-content";
|
||||
import ModalHeader from "./modal-header";
|
||||
import ModalBody from "./modal-body";
|
||||
import ModalFooter from "./modal-footer";
|
||||
|
||||
// export types
|
||||
export type {ModalProps} from "./modal";
|
||||
export type {ModalTriggerProps} from "./modal-trigger";
|
||||
export type {ModalContentProps} from "./modal-content";
|
||||
export type {ModalHeaderProps} from "./modal-header";
|
||||
export type {ModalBodyProps} from "./modal-body";
|
||||
export type {ModalFooterProps} from "./modal-footer";
|
||||
|
||||
// export hooks
|
||||
export {useModal} from "./use-modal";
|
||||
@ -14,4 +20,4 @@ export {useModal} from "./use-modal";
|
||||
export * from "./modal-context";
|
||||
|
||||
// export components
|
||||
export {Modal, ModalTrigger, ModalContent};
|
||||
export {Modal, ModalTrigger, ModalContent, ModalHeader, ModalBody, ModalFooter};
|
||||
|
||||
43
packages/components/modal/src/modal-body.tsx
Normal file
43
packages/components/modal/src/modal-body.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import {useEffect} from "react";
|
||||
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
|
||||
import {useModalContext} from "./modal-context";
|
||||
|
||||
export interface ModalBodyProps extends HTMLNextUIProps<"div"> {}
|
||||
|
||||
const ModalBody = forwardRef<ModalBodyProps, "div">((props, ref) => {
|
||||
const {as, children, className, ...otherProps} = props;
|
||||
|
||||
const {slots, classNames, bodyId, setBodyMounted} = useModalContext();
|
||||
|
||||
const domRef = useDOMRef(ref);
|
||||
|
||||
const Component = as || "div";
|
||||
|
||||
/**
|
||||
* Notify us if this component was rendered or used,
|
||||
* so we can append `aria-labelledby` automatically
|
||||
*/
|
||||
useEffect(() => {
|
||||
setBodyMounted(true);
|
||||
|
||||
return () => setBodyMounted(false);
|
||||
}, [setBodyMounted]);
|
||||
|
||||
return (
|
||||
<Component
|
||||
ref={domRef}
|
||||
className={slots.body({class: clsx(classNames?.body, className)})}
|
||||
id={bodyId}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
});
|
||||
|
||||
ModalBody.displayName = "NextUI.ModalBody";
|
||||
|
||||
export default ModalBody;
|
||||
@ -1,20 +1,19 @@
|
||||
import type {AriaDialogProps} from "@react-aria/dialog";
|
||||
import type {HTMLMotionProps} from "framer-motion";
|
||||
|
||||
import {DOMAttributes, ReactNode, useMemo} from "react";
|
||||
import {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 {CloseIcon} from "@nextui-org/shared-icons";
|
||||
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);
|
||||
children: ReactNode | ((onClose: () => void) => ReactNode);
|
||||
}
|
||||
|
||||
const ModalContent = forwardRef<ModalContentProps, "section">((props, _) => {
|
||||
@ -27,15 +26,17 @@ const ModalContent = forwardRef<ModalContentProps, "section">((props, _) => {
|
||||
classNames,
|
||||
motionProps,
|
||||
backdropVariant,
|
||||
showCloseButton,
|
||||
disableAnimation,
|
||||
getDialogProps,
|
||||
getBackdropProps,
|
||||
getCloseButtonProps,
|
||||
onClose,
|
||||
} = useModalContext();
|
||||
|
||||
const Component = as || DialogComponent || "section";
|
||||
|
||||
const {dialogProps, titleProps} = useDialog(
|
||||
const {dialogProps} = useDialog(
|
||||
{
|
||||
role: "dialog",
|
||||
},
|
||||
@ -46,9 +47,12 @@ const ModalContent = forwardRef<ModalContentProps, "section">((props, _) => {
|
||||
<>
|
||||
<DismissButton onDismiss={onClose} />
|
||||
<Component {...getDialogProps(mergeProps(dialogProps, otherProps))}>
|
||||
<FocusScope contain restoreFocus>
|
||||
{typeof children === "function" ? children(titleProps) : children}
|
||||
</FocusScope>
|
||||
{showCloseButton && (
|
||||
<button {...getCloseButtonProps()}>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
)}
|
||||
{typeof children === "function" ? children(onClose) : children}
|
||||
</Component>
|
||||
<DismissButton onDismiss={onClose} />
|
||||
</>
|
||||
|
||||
31
packages/components/modal/src/modal-footer.tsx
Normal file
31
packages/components/modal/src/modal-footer.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
|
||||
import {useModalContext} from "./modal-context";
|
||||
|
||||
export interface ModalFooterProps extends HTMLNextUIProps<"footer"> {}
|
||||
|
||||
const ModalFooter = forwardRef<ModalFooterProps, "footer">((props, ref) => {
|
||||
const {as, children, className, ...otherProps} = props;
|
||||
|
||||
const {slots, classNames} = useModalContext();
|
||||
|
||||
const domRef = useDOMRef(ref);
|
||||
|
||||
const Component = as || "footer";
|
||||
|
||||
return (
|
||||
<Component
|
||||
ref={domRef}
|
||||
className={slots.footer({class: clsx(classNames?.footer, className)})}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
});
|
||||
|
||||
ModalFooter.displayName = "NextUI.ModalFooter";
|
||||
|
||||
export default ModalFooter;
|
||||
43
packages/components/modal/src/modal-header.tsx
Normal file
43
packages/components/modal/src/modal-header.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import {useEffect} from "react";
|
||||
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
|
||||
import {useModalContext} from "./modal-context";
|
||||
|
||||
export interface ModalHeaderProps extends HTMLNextUIProps<"header"> {}
|
||||
|
||||
const ModalHeader = forwardRef<ModalHeaderProps, "header">((props, ref) => {
|
||||
const {as, children, className, ...otherProps} = props;
|
||||
|
||||
const {slots, classNames, headerId, setHeaderMounted} = useModalContext();
|
||||
|
||||
const domRef = useDOMRef(ref);
|
||||
|
||||
const Component = as || "header";
|
||||
|
||||
/**
|
||||
* Notify us if this component was rendered or used,
|
||||
* so we can append `aria-labelledby` automatically
|
||||
*/
|
||||
useEffect(() => {
|
||||
setHeaderMounted(true);
|
||||
|
||||
return () => setHeaderMounted(false);
|
||||
}, [setHeaderMounted]);
|
||||
|
||||
return (
|
||||
<Component
|
||||
ref={domRef}
|
||||
className={slots.header({class: clsx(classNames?.header, className)})}
|
||||
id={headerId}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
});
|
||||
|
||||
ModalHeader.displayName = "NextUI.ModalHeader";
|
||||
|
||||
export default ModalHeader;
|
||||
@ -12,8 +12,10 @@ import {
|
||||
useMemo,
|
||||
useImperativeHandle,
|
||||
} from "react";
|
||||
import {HTMLNextUIProps, mapPropsVariants, PropGetter} from "@nextui-org/system";
|
||||
import {modal} from "@nextui-org/theme";
|
||||
import {HTMLNextUIProps, mapPropsVariants, PropGetter} from "@nextui-org/system";
|
||||
import {useAriaButton} from "@nextui-org/use-aria-button";
|
||||
import {useFocusRing} from "@react-aria/focus";
|
||||
import {clsx, ReactRef} from "@nextui-org/shared-utils";
|
||||
import {useOverlayTrigger} from "@react-aria/overlays";
|
||||
import {createDOMRef} from "@nextui-org/dom-utils";
|
||||
@ -34,7 +36,11 @@ 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">;
|
||||
|
||||
/**
|
||||
* Determines if the modal should have a close button in the top right corner.
|
||||
* @default true
|
||||
*/
|
||||
showCloseButton?: boolean;
|
||||
/**
|
||||
* Whether the animation should be disabled.
|
||||
* @default false
|
||||
@ -47,10 +53,13 @@ interface Props extends HTMLNextUIProps<"div"> {
|
||||
* @example
|
||||
* ```ts
|
||||
* <Modal classNames={{
|
||||
* wrapper: "wrapper-classes", // main wrapper
|
||||
* backdrop: "backdrop-classes",
|
||||
* base:"base-classes",
|
||||
* header: "header-classes",
|
||||
* body: "body-classes",
|
||||
* footer: "footer-classes",
|
||||
* closeButton: "close-button-classes",
|
||||
* }} />
|
||||
* ```
|
||||
*/
|
||||
@ -73,6 +82,9 @@ export function useModal(originalProps: UseModalProps) {
|
||||
defaultOpen,
|
||||
onOpenChange,
|
||||
motionProps,
|
||||
isDismissable = true,
|
||||
showCloseButton = true,
|
||||
isKeyboardDismissDisabled = false,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
@ -80,6 +92,7 @@ export function useModal(originalProps: UseModalProps) {
|
||||
|
||||
const dialogRef = useRef<HTMLElement>(null);
|
||||
const domTriggerRef = useRef<HTMLElement>(null);
|
||||
const closeButtonRef = useRef<HTMLElement>(null);
|
||||
|
||||
const [headerMounted, setHeaderMounted] = useState(false);
|
||||
const [bodyMounted, setBodyMounted] = useState(false);
|
||||
@ -104,7 +117,20 @@ export function useModal(originalProps: UseModalProps) {
|
||||
|
||||
const {triggerProps} = useOverlayTrigger({type: "dialog"}, state, triggerRef);
|
||||
|
||||
const {modalProps, underlayProps} = useModalOverlay(originalProps, state, dialogRef);
|
||||
const {modalProps, underlayProps} = useModalOverlay(
|
||||
{
|
||||
isDismissable,
|
||||
isKeyboardDismissDisabled,
|
||||
},
|
||||
state,
|
||||
dialogRef,
|
||||
);
|
||||
|
||||
const {buttonProps: closeButtonProps} = useAriaButton({onPress: state.close}, closeButtonRef);
|
||||
const {
|
||||
isFocusVisible: isCloseButtonFocusVisible,
|
||||
focusProps: closeButtonFocusProps,
|
||||
} = useFocusRing();
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
|
||||
@ -112,35 +138,28 @@ export function useModal(originalProps: UseModalProps) {
|
||||
() =>
|
||||
modal({
|
||||
...variantProps,
|
||||
isCloseButtonFocusVisible,
|
||||
}),
|
||||
[...Object.values(variantProps)],
|
||||
[...Object.values(variantProps), isCloseButtonFocusVisible],
|
||||
);
|
||||
|
||||
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 getDialogProps: PropGetter = (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 getTriggerProps: PropGetter = (props = {}, _ref: Ref<any> | null | undefined = null) => ({
|
||||
...mergeProps(triggerProps, props),
|
||||
className: slots.trigger({class: clsx(classNames?.trigger, props.className)}),
|
||||
ref: mergeRefs(_ref, triggerRef),
|
||||
"aria-controls": dialogId,
|
||||
"aria-haspopup": "dialog",
|
||||
});
|
||||
|
||||
const getBackdropProps = useCallback<PropGetter>(
|
||||
(props = {}) => ({
|
||||
@ -152,6 +171,15 @@ export function useModal(originalProps: UseModalProps) {
|
||||
[slots, classNames, underlayProps],
|
||||
);
|
||||
|
||||
const getCloseButtonProps: PropGetter = () => {
|
||||
return {
|
||||
role: "button",
|
||||
tabIndex: 0,
|
||||
className: slots.closeButton({class: classNames?.closeButton}),
|
||||
...mergeProps(closeButtonProps, closeButtonFocusProps),
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
Component,
|
||||
slots,
|
||||
@ -161,6 +189,8 @@ export function useModal(originalProps: UseModalProps) {
|
||||
triggerRef,
|
||||
motionProps,
|
||||
classNames,
|
||||
isDismissable,
|
||||
showCloseButton,
|
||||
backdropVariant: originalProps.backdropVariant ?? "opaque",
|
||||
isOpen: state.isOpen,
|
||||
onClose: state.close,
|
||||
@ -170,6 +200,7 @@ export function useModal(originalProps: UseModalProps) {
|
||||
getDialogProps,
|
||||
getTriggerProps,
|
||||
getBackdropProps,
|
||||
getCloseButtonProps,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,23 @@
|
||||
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||
/* eslint-disable jsx-a11y/no-autofocus */
|
||||
import React from "react";
|
||||
import {ComponentStory, ComponentMeta} from "@storybook/react";
|
||||
import {modal} from "@nextui-org/theme";
|
||||
import {link, modal} from "@nextui-org/theme";
|
||||
import {Button} from "@nextui-org/button";
|
||||
import {Input} from "@nextui-org/input";
|
||||
import {Checkbox} from "@nextui-org/checkbox";
|
||||
import {MailFilledIcon, LockFilledIcon} from "@nextui-org/shared-icons";
|
||||
import Lorem from "react-lorem-component";
|
||||
|
||||
import {Modal, ModalContent, ModalTrigger, ModalProps} from "../src";
|
||||
import {
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalTrigger,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalProps,
|
||||
} from "../src";
|
||||
|
||||
export default {
|
||||
title: "Components/Modal",
|
||||
@ -32,6 +46,16 @@ export default {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
isDismissable: {
|
||||
control: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
isKeyboardDismissDisabled: {
|
||||
control: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
children: {
|
||||
control: {
|
||||
disable: true,
|
||||
@ -49,25 +73,146 @@ export default {
|
||||
|
||||
const defaultProps = {
|
||||
...modal.defaultVariants,
|
||||
disableAnimation: false,
|
||||
isDismissable: true,
|
||||
isKeyboardDismissDisabled: false,
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
const content = (
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader className="flex flex-col gap-1">Log in</ModalHeader>
|
||||
<ModalBody>
|
||||
<Input
|
||||
autoFocus
|
||||
endContent={
|
||||
<MailFilledIcon className="text-2xl text-neutral-400 pointer-events-none flex-shrink-0" />
|
||||
}
|
||||
label="Email"
|
||||
placeholder="Enter your email"
|
||||
variant="bordered"
|
||||
/>
|
||||
<Input
|
||||
endContent={
|
||||
<LockFilledIcon className="text-2xl text-neutral-400 pointer-events-none flex-shrink-0" />
|
||||
}
|
||||
label="Password"
|
||||
placeholder="Enter your password"
|
||||
type="password"
|
||||
variant="bordered"
|
||||
/>
|
||||
<div className="flex py-2 px-1 justify-between">
|
||||
<Checkbox
|
||||
classNames={{
|
||||
label: "text-sm",
|
||||
}}
|
||||
>
|
||||
Remember me
|
||||
</Checkbox>
|
||||
<a className={link({size: "sm"})} href="#">
|
||||
Forgot password?
|
||||
</a>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" variant="flat" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
<Button color="primary" onPress={onClose}>
|
||||
Sign in
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
);
|
||||
|
||||
const Template: ComponentStory<typeof Modal> = (args: ModalProps) => (
|
||||
<Modal {...args}>
|
||||
<ModalTrigger>
|
||||
<Button disableAnimation={!!args.disableAnimation}>Open Modal</Button>
|
||||
</ModalTrigger>
|
||||
{content}
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const InsideScrollTemplate: ComponentStory<typeof Modal> = (args: ModalProps) => (
|
||||
<Modal {...args}>
|
||||
<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>
|
||||
);
|
||||
|
||||
const OutsideScrollTemplate: ComponentStory<typeof Modal> = (args: ModalProps) => (
|
||||
<Modal {...args} scrollBehavior="outside">
|
||||
<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>
|
||||
);
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
...defaultProps,
|
||||
};
|
||||
|
||||
export const InsideScroll = InsideScrollTemplate.bind({});
|
||||
InsideScroll.args = {
|
||||
...defaultProps,
|
||||
};
|
||||
|
||||
export const OutsideScroll = OutsideScrollTemplate.bind({});
|
||||
OutsideScroll.args = {
|
||||
...defaultProps,
|
||||
};
|
||||
|
||||
export const DisableAnimation = Template.bind({});
|
||||
DisableAnimation.args = {
|
||||
...defaultProps,
|
||||
disableAnimation: true,
|
||||
};
|
||||
|
||||
export const CustomMotion = Template.bind({});
|
||||
CustomMotion.args = {
|
||||
...defaultProps,
|
||||
motionProps: {
|
||||
variants: {
|
||||
enter: {
|
||||
opacity: 1,
|
||||
duration: 0.3,
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
duration: 0.3,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -10,12 +10,11 @@ import {motion} from "framer-motion";
|
||||
import {getTransformOrigins} from "@nextui-org/aria-utils";
|
||||
import {useDialog} from "@react-aria/dialog";
|
||||
import {mergeProps} from "@react-aria/utils";
|
||||
import {FocusableElement} from "@react-types/shared";
|
||||
|
||||
import {usePopoverContext} from "./popover-context";
|
||||
|
||||
export interface PopoverContentProps extends AriaDialogProps {
|
||||
children: ReactNode | ((titleProps: DOMAttributes<FocusableElement>) => ReactNode);
|
||||
children: ReactNode | ((titleProps: DOMAttributes<HTMLElement>) => ReactNode);
|
||||
}
|
||||
|
||||
const PopoverContent = forwardRef<PopoverContentProps, "section">((props, _) => {
|
||||
|
||||
@ -110,7 +110,7 @@ const Template: ComponentStory<typeof Popover> = (args: PopoverProps) => {
|
||||
return (
|
||||
<Popover {...args}>
|
||||
<PopoverTrigger>
|
||||
<Button disableAnimation={!!args.disableAnimation}>Open popover</Button>
|
||||
<Button disableAnimation={!!args.disableAnimation}>Open Popover</Button>
|
||||
</PopoverTrigger>
|
||||
{content}
|
||||
</Popover>
|
||||
@ -121,7 +121,7 @@ const WithTitlePropsTemplate: ComponentStory<typeof Popover> = (args: PopoverPro
|
||||
return (
|
||||
<Popover {...args}>
|
||||
<PopoverTrigger>
|
||||
<Button disableAnimation={!!args.disableAnimation}>Open popover</Button>
|
||||
<Button disableAnimation={!!args.disableAnimation}>Open Popover</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
{(titleProps) => (
|
||||
@ -144,7 +144,7 @@ const OpenChangeTemplate: ComponentStory<typeof Popover> = (args: PopoverProps)
|
||||
<div className="flex flex-col gap-2">
|
||||
<Popover {...args} onOpenChange={(open) => setIsOpen(open)}>
|
||||
<PopoverTrigger>
|
||||
<Button>Open popover</Button>
|
||||
<Button>Open Popover</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<div className="px-1 py-2">
|
||||
@ -370,7 +370,7 @@ const OffsetTemplate: ComponentStory<typeof Popover> = (args: PopoverProps) => (
|
||||
const WithFormTemplate: ComponentStory<typeof Popover> = (args: PopoverProps) => (
|
||||
<Popover {...args}>
|
||||
<PopoverTrigger>
|
||||
<Button color="primary">Open popover</Button>
|
||||
<Button color="primary">Open Popover</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
{(titleProps) => (
|
||||
@ -416,7 +416,7 @@ const WithBackdropTemplate: ComponentStory<typeof Popover> = (args: PopoverProps
|
||||
<Popover {...args}>
|
||||
<PopoverTrigger>
|
||||
<Button color="primary" radius="full">
|
||||
Open popover
|
||||
Open Popover
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
@ -510,3 +510,21 @@ WithBackdrop.args = {
|
||||
backdropVariant: "blur",
|
||||
className: "bg-white dark:bg-content1",
|
||||
};
|
||||
|
||||
export const CustomMotion = Template.bind({});
|
||||
CustomMotion.args = {
|
||||
...defaultProps,
|
||||
placement: "bottom",
|
||||
motionProps: {
|
||||
variants: {
|
||||
enter: {
|
||||
opacity: 1,
|
||||
duration: 0.2,
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
duration: 0.1,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -2,7 +2,7 @@ import type {VariantProps} from "tailwind-variants";
|
||||
|
||||
import {tv} from "tailwind-variants";
|
||||
|
||||
// import {ringClasses} from "../utils";
|
||||
import {ringClasses} from "../utils";
|
||||
|
||||
/**
|
||||
* Card **Tailwind Variants** component
|
||||
@ -35,9 +35,10 @@ const modal = tv({
|
||||
"overflow-y-hidden",
|
||||
"justify-center",
|
||||
"items-center",
|
||||
"outline-none",
|
||||
],
|
||||
base: [
|
||||
"flex",
|
||||
"flex-col",
|
||||
"relative",
|
||||
"bg-white",
|
||||
"z-50",
|
||||
@ -46,12 +47,26 @@ const modal = tv({
|
||||
"box-border",
|
||||
"dark:bg-content1",
|
||||
"border border-neutral-100",
|
||||
"outline-none",
|
||||
],
|
||||
trigger: [],
|
||||
backdrop: ["hidden"],
|
||||
header: [],
|
||||
body: [],
|
||||
footer: [],
|
||||
header: "flex py-4 px-6 flex-initial text-lg font-semibold",
|
||||
body: "flex flex-1 flex-col gap-3 px-6 py-2",
|
||||
footer: "flex flex-row gap-2 px-6 py-4 justify-end",
|
||||
closeButton: [
|
||||
"absolute",
|
||||
"appearance-none",
|
||||
"outline-none",
|
||||
"select-none",
|
||||
"top-1",
|
||||
"right-1",
|
||||
"p-2",
|
||||
"text-neutral-500",
|
||||
"rounded-full",
|
||||
"hover:bg-neutral-100",
|
||||
"active:bg-neutral-200",
|
||||
],
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
@ -83,10 +98,7 @@ const modal = tv({
|
||||
base: "max-w-5xl",
|
||||
},
|
||||
full: {
|
||||
base: "max-w-full",
|
||||
},
|
||||
prose: {
|
||||
base: "max-w-prose",
|
||||
base: "max-w-full rounded-none min-h-screen",
|
||||
},
|
||||
},
|
||||
radius: {
|
||||
@ -108,11 +120,27 @@ const modal = tv({
|
||||
backdrop: "backdrop-blur-sm backdrop-saturate-150 bg-black/20",
|
||||
},
|
||||
},
|
||||
scrollBehavior: {
|
||||
inside: {
|
||||
base: "max-h-[calc(100%_-_7.5rem)]",
|
||||
body: "overflow-y-auto",
|
||||
},
|
||||
outside: {
|
||||
wrapper: "items-start overflow-y-auto",
|
||||
base: "my-16",
|
||||
},
|
||||
},
|
||||
isCloseButtonFocusVisible: {
|
||||
true: {
|
||||
closeButton: [...ringClasses],
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "md",
|
||||
radius: "lg",
|
||||
backdropVariant: "opaque",
|
||||
backdropVariant: "blur",
|
||||
scrollBehavior: "inside",
|
||||
},
|
||||
compoundVariants: [
|
||||
// backdropVariant (opaque/blur)
|
||||
|
||||
@ -23,3 +23,4 @@ export * from "./eye-filled";
|
||||
export * from "./eye-slash-filled";
|
||||
export * from "./search";
|
||||
export * from "./bulk";
|
||||
export * from "./lock-filled";
|
||||
|
||||
23
packages/utilities/shared-icons/src/lock-filled.tsx
Normal file
23
packages/utilities/shared-icons/src/lock-filled.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import {IconSvgProps} from "./types";
|
||||
|
||||
export const LockFilledIcon = (props: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M12.0011 17.3498C12.9013 17.3498 13.6311 16.6201 13.6311 15.7198C13.6311 14.8196 12.9013 14.0898 12.0011 14.0898C11.1009 14.0898 10.3711 14.8196 10.3711 15.7198C10.3711 16.6201 11.1009 17.3498 12.0011 17.3498Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M18.28 9.53V8.28C18.28 5.58 17.63 2 12 2C6.37 2 5.72 5.58 5.72 8.28V9.53C2.92 9.88 2 11.3 2 14.79V16.65C2 20.75 3.25 22 7.35 22H16.65C20.75 22 22 20.75 22 16.65V14.79C22 11.3 21.08 9.88 18.28 9.53ZM12 18.74C10.33 18.74 8.98 17.38 8.98 15.72C8.98 14.05 10.34 12.7 12 12.7C13.66 12.7 15.02 14.06 15.02 15.72C15.02 17.39 13.67 18.74 12 18.74ZM7.35 9.44C7.27 9.44 7.2 9.44 7.12 9.44V8.28C7.12 5.35 7.95 3.4 12 3.4C16.05 3.4 16.88 5.35 16.88 8.28V9.45C16.8 9.45 16.73 9.45 16.65 9.45H7.35V9.44Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
39
pnpm-lock.yaml
generated
39
pnpm-lock.yaml
generated
@ -1015,6 +1015,9 @@ importers:
|
||||
'@nextui-org/framer-transitions':
|
||||
specifier: workspace:*
|
||||
version: link:../../utilities/framer-transitions
|
||||
'@nextui-org/shared-icons':
|
||||
specifier: workspace:*
|
||||
version: link:../../utilities/shared-icons
|
||||
'@nextui-org/shared-utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../utilities/shared-utils
|
||||
@ -1046,6 +1049,9 @@ importers:
|
||||
specifier: ^3.5.1
|
||||
version: 3.5.1(react@18.2.0)
|
||||
devDependencies:
|
||||
'@nextui-org/checkbox':
|
||||
specifier: workspace:*
|
||||
version: link:../checkbox
|
||||
'@nextui-org/input':
|
||||
specifier: workspace:*
|
||||
version: link:../input
|
||||
@ -1061,6 +1067,9 @@ importers:
|
||||
react:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0
|
||||
react-lorem-component:
|
||||
specifier: 0.13.0
|
||||
version: 0.13.0(react@18.2.0)
|
||||
|
||||
packages/components/pagination:
|
||||
dependencies:
|
||||
@ -11652,6 +11661,13 @@ packages:
|
||||
sha.js: 2.4.11
|
||||
dev: true
|
||||
|
||||
/create-react-class@15.7.0:
|
||||
resolution: {integrity: sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng==}
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
object-assign: 4.1.1
|
||||
dev: true
|
||||
|
||||
/create-require@1.1.1:
|
||||
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
|
||||
|
||||
@ -17170,6 +17186,13 @@ packages:
|
||||
dependencies:
|
||||
js-tokens: 4.0.0
|
||||
|
||||
/lorem-ipsum@1.0.6:
|
||||
resolution: {integrity: sha512-Rx4XH8X4KSDCKAVvWGYlhAfNqdUP5ZdT4rRyf0jjrvWgtViZimDIlopWNfn/y3lGM5K4uuiAoY28TaD+7YKFrQ==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
minimist: 1.2.8
|
||||
dev: true
|
||||
|
||||
/loud-rejection@1.6.0:
|
||||
resolution: {integrity: sha512-RPNliZOFkqFumDhvYqOaNY4Uz9oJM2K9tC6JWsJJsNdhuONW4LQHRBpb0qf4pJApVffI5N39SwzWZJuEhfd7eQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -20073,6 +20096,18 @@ packages:
|
||||
unescape: 1.0.1
|
||||
dev: false
|
||||
|
||||
/react-lorem-component@0.13.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-4mWjxmcG/DJJwdxdKwXWyP2N9zohbJg/yYaC+7JffQNrKj3LYDpA/A4u/Dju1v1ZF6Jew2gbFKGb5Z6CL+UNTw==}
|
||||
peerDependencies:
|
||||
react: 16.x
|
||||
dependencies:
|
||||
create-react-class: 15.7.0
|
||||
lorem-ipsum: 1.0.6
|
||||
object-assign: 4.1.1
|
||||
react: 18.2.0
|
||||
seedable-random: 0.0.1
|
||||
dev: true
|
||||
|
||||
/react-markdown@6.0.2(@types/react@17.0.11)(react@18.2.0):
|
||||
resolution: {integrity: sha512-Et2AjXAsbmPP1nLQQRqmVgcqzfwcz8uQJ8VAdADs8Nk/aaUA0YeU9RDLuCtD+GwajCnm/+Iiu2KPmXzmD/M3vA==}
|
||||
peerDependencies:
|
||||
@ -20894,6 +20929,10 @@ packages:
|
||||
extend-shallow: 2.0.1
|
||||
kind-of: 6.0.3
|
||||
|
||||
/seedable-random@0.0.1:
|
||||
resolution: {integrity: sha512-uZWbEfz3BQdBl4QlUPELPqhInGEO1Q6zjzqrTDkd3j7mHaWWJo7h4ydr2g24a2WtTLk3imTLc8mPbBdQqdsbGw==}
|
||||
dev: true
|
||||
|
||||
/select@1.1.2:
|
||||
resolution: {integrity: sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==}
|
||||
dev: false
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user