From 44f81076197016341c83661c6bbdb98d28b8c20c Mon Sep 17 00:00:00 2001 From: Muhammad Amien Date: Fri, 23 Feb 2024 09:49:43 +0700 Subject: [PATCH] feat: modal component --- packages/components/accordion/README.md | 6 +- packages/components/modal/README.md | 24 ++ .../components/modal/__tests__/modal.test.tsx | 28 ++ packages/components/modal/package.json | 73 ++++ packages/components/modal/src/index.ts | 23 ++ packages/components/modal/src/modal-body.tsx | 43 +++ .../components/modal/src/modal-content.tsx | 116 ++++++ .../components/modal/src/modal-context.ts | 9 + .../components/modal/src/modal-footer.tsx | 31 ++ .../components/modal/src/modal-header.tsx | 43 +++ .../components/modal/src/modal-transition.ts | 33 ++ packages/components/modal/src/modal.tsx | 35 ++ packages/components/modal/src/use-modal.ts | 203 ++++++++++ .../modal/stories/modal.stories.tsx | 364 ++++++++++++++++++ packages/components/modal/tsconfig.json | 10 + packages/components/modal/tsup.config.ts | 8 + packages/core/theme/src/components/index.ts | 2 +- packages/core/theme/src/components/modal.ts | 189 +++++++++ .../hooks/use-aria-modal-overlay/README.md | 24 ++ .../hooks/use-aria-modal-overlay/package.json | 61 +++ .../hooks/use-aria-modal-overlay/src/index.ts | 42 ++ .../use-aria-modal-overlay/tsconfig.json | 4 + packages/hooks/use-callback-ref/README.md | 24 ++ packages/hooks/use-callback-ref/package.json | 55 +++ packages/hooks/use-callback-ref/src/index.ts | 27 ++ packages/hooks/use-callback-ref/tsconfig.json | 4 + packages/hooks/use-clipboard/README.md | 24 ++ .../__tests__/clipboard.test.tsx | 38 ++ packages/hooks/use-clipboard/package.json | 52 +++ packages/hooks/use-clipboard/src/index.ts | 53 +++ packages/hooks/use-clipboard/tsconfig.json | 4 + packages/hooks/use-disclosure/README.md | 24 ++ packages/hooks/use-disclosure/package.json | 57 +++ packages/hooks/use-disclosure/src/index.ts | 73 ++++ packages/hooks/use-disclosure/tsconfig.json | 4 + pnpm-lock.yaml | 148 +++++++ 36 files changed, 1954 insertions(+), 4 deletions(-) create mode 100644 packages/components/modal/README.md create mode 100644 packages/components/modal/__tests__/modal.test.tsx create mode 100644 packages/components/modal/package.json create mode 100644 packages/components/modal/src/index.ts create mode 100644 packages/components/modal/src/modal-body.tsx create mode 100644 packages/components/modal/src/modal-content.tsx create mode 100644 packages/components/modal/src/modal-context.ts create mode 100644 packages/components/modal/src/modal-footer.tsx create mode 100644 packages/components/modal/src/modal-header.tsx create mode 100644 packages/components/modal/src/modal-transition.ts create mode 100644 packages/components/modal/src/modal.tsx create mode 100644 packages/components/modal/src/use-modal.ts create mode 100644 packages/components/modal/stories/modal.stories.tsx create mode 100644 packages/components/modal/tsconfig.json create mode 100644 packages/components/modal/tsup.config.ts create mode 100644 packages/core/theme/src/components/modal.ts create mode 100644 packages/hooks/use-aria-modal-overlay/README.md create mode 100644 packages/hooks/use-aria-modal-overlay/package.json create mode 100644 packages/hooks/use-aria-modal-overlay/src/index.ts create mode 100644 packages/hooks/use-aria-modal-overlay/tsconfig.json create mode 100644 packages/hooks/use-callback-ref/README.md create mode 100644 packages/hooks/use-callback-ref/package.json create mode 100644 packages/hooks/use-callback-ref/src/index.ts create mode 100644 packages/hooks/use-callback-ref/tsconfig.json create mode 100644 packages/hooks/use-clipboard/README.md create mode 100644 packages/hooks/use-clipboard/__tests__/clipboard.test.tsx create mode 100644 packages/hooks/use-clipboard/package.json create mode 100644 packages/hooks/use-clipboard/src/index.ts create mode 100644 packages/hooks/use-clipboard/tsconfig.json create mode 100644 packages/hooks/use-disclosure/README.md create mode 100644 packages/hooks/use-disclosure/package.json create mode 100644 packages/hooks/use-disclosure/src/index.ts create mode 100644 packages/hooks/use-disclosure/tsconfig.json diff --git a/packages/components/accordion/README.md b/packages/components/accordion/README.md index cb58e99b..d8fdf7fc 100644 --- a/packages/components/accordion/README.md +++ b/packages/components/accordion/README.md @@ -1,4 +1,4 @@ -# @nextui-org/accordion +# @jala-banyu/accordion Accordion display a list of high-level options that can expand/collapse to reveal more information. @@ -7,9 +7,9 @@ Please refer to the [documentation](https://nextui.org//docs/components/accordio ## Installation ```sh -yarn add @nextui-org/accordion +yarn add @jala-banyu/accordion # or -npm i @nextui-org/accordion +npm i @jala-banyu/accordion ``` ## Contribution diff --git a/packages/components/modal/README.md b/packages/components/modal/README.md new file mode 100644 index 00000000..6684b141 --- /dev/null +++ b/packages/components/modal/README.md @@ -0,0 +1,24 @@ +# @jala-banyu/modal + +A Quick description of the component + +> This is an internal utility, not intended for public usage. + +## Installation + +```sh +yarn add @jala-banyu/modal +# or +npm i @jala-banyu/modal +``` + +## Contribution + +Yes please! See the +[contributing guidelines](https://github.com/Atnic/banyu/blob/master/CONTRIBUTING.md) +for details. + +## Licence + +This project is licensed under the terms of the +[MIT license](https://github.com/Atnic/banyu/blob/master/LICENSE). diff --git a/packages/components/modal/__tests__/modal.test.tsx b/packages/components/modal/__tests__/modal.test.tsx new file mode 100644 index 00000000..05ca441f --- /dev/null +++ b/packages/components/modal/__tests__/modal.test.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; +import {render} from "@testing-library/react"; + +import {Modal} from "../src"; + +describe("Modal", () => { + it("should render correctly", () => { + // eslint-disable-next-line react/no-children-prop + const wrapper = render( + + test + , + ); + + expect(() => wrapper.unmount()).not.toThrow(); + }); + + it("ref should be forwarded", () => { + const ref = React.createRef(); + + render( + + test + , + ); + // expect(ref.current).not.toBeNull(); + }); +}); diff --git a/packages/components/modal/package.json b/packages/components/modal/package.json new file mode 100644 index 00000000..8200cd59 --- /dev/null +++ b/packages/components/modal/package.json @@ -0,0 +1,73 @@ +{ + "name": "@jala-banyu/modal", + "version": "0.0.0", + "description": "Displays a dialog with a custom content that requires attention or provides additional information.", + "keywords": [ + "modal" + ], + "author": "Muhammad Amien ", + "homepage": "#", + "license": "MIT", + "main": "src/index.ts", + "sideEffects": false, + "files": [ + "dist" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Atnic/banyu.git", + "directory": "packages/components/modal" + }, + "bugs": { + "url": "https://github.com/Atnic/banyu/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", + "react-dom": ">=18", + "@jala-banyu/theme": ">=1.2.0", + "@jala-banyu/system": ">=1.0.0" + }, + "dependencies": { + "@jala-banyu/use-disclosure": "workspace:*", + "@jala-banyu/use-aria-button": "workspace:*", + "@jala-banyu/framer-transitions": "workspace:*", + "@jala-banyu/shared-utils": "workspace:*", + "@jala-banyu/react-utils": "workspace:*", + "@jala-banyu/shared-icons": "workspace:*", + "@jala-banyu/use-aria-modal-overlay": "workspace:*", + "@react-aria/dialog": "^3.5.7", + "@react-aria/interactions": "^3.19.1", + "@react-aria/overlays": "^3.18.1", + "@react-aria/utils": "^3.21.1", + "@react-stately/overlays": "^3.6.3", + "@react-aria/focus": "^3.14.3", + "@react-types/overlays": "^3.8.3", + "react-remove-scroll": "^2.5.6" + }, + "devDependencies": { + "@jala-banyu/theme": "workspace:*", + "@jala-banyu/system": "workspace:*", + "@jala-banyu/input": "workspace:*", + "@jala-banyu/checkbox": "workspace:*", + "@jala-banyu/button": "workspace:*", + "@jala-banyu/link": "workspace:*", + "react-lorem-component": "0.13.0", + "framer-motion": "^10.16.4", + "clean-package": "2.2.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "clean-package": "../../../clean-package.config.json" +} diff --git a/packages/components/modal/src/index.ts b/packages/components/modal/src/index.ts new file mode 100644 index 00000000..64bca1fb --- /dev/null +++ b/packages/components/modal/src/index.ts @@ -0,0 +1,23 @@ +import Modal from "./modal"; +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 {ModalContentProps} from "./modal-content"; +export type {ModalHeaderProps} from "./modal-header"; +export type {ModalBodyProps} from "./modal-body"; +export type {ModalFooterProps} from "./modal-footer"; +export type {UseDisclosureProps} from "@jala-banyu/use-disclosure"; + +// export hooks +export {useModal} from "./use-modal"; +export {useDisclosure} from "@jala-banyu/use-disclosure"; + +// export context +export {ModalProvider, useModalContext} from "./modal-context"; + +// export components +export {Modal, ModalContent, ModalHeader, ModalBody, ModalFooter}; diff --git a/packages/components/modal/src/modal-body.tsx b/packages/components/modal/src/modal-body.tsx new file mode 100644 index 00000000..0d2bb26f --- /dev/null +++ b/packages/components/modal/src/modal-body.tsx @@ -0,0 +1,43 @@ +import {useEffect} from "react"; +import {forwardRef, HTMLBanyuProps} from "@jala-banyu/system"; +import {useDOMRef} from "@jala-banyu/react-utils"; +import {clsx} from "@jala-banyu/shared-utils"; + +import {useModalContext} from "./modal-context"; + +export interface ModalBodyProps extends HTMLBanyuProps<"div"> {} + +const ModalBody = forwardRef<"div", ModalBodyProps>((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 ( + + {children} + + ); +}); + +ModalBody.displayName = "Banyu.ModalBody"; + +export default ModalBody; diff --git a/packages/components/modal/src/modal-content.tsx b/packages/components/modal/src/modal-content.tsx new file mode 100644 index 00000000..a4654832 --- /dev/null +++ b/packages/components/modal/src/modal-content.tsx @@ -0,0 +1,116 @@ +import type {AriaDialogProps} from "@react-aria/dialog"; +import type {HTMLMotionProps} from "framer-motion"; + +import {cloneElement, isValidElement, ReactNode, useMemo} from "react"; +import {forwardRef} from "@jala-banyu/system"; +import {DismissButton} from "@react-aria/overlays"; +import {TRANSITION_VARIANTS} from "@jala-banyu/framer-transitions"; +import {CloseIcon} from "@jala-banyu/shared-icons"; +import {RemoveScroll} from "react-remove-scroll"; +import {motion} from "framer-motion"; +import {useDialog} from "@react-aria/dialog"; +import {mergeProps} from "@react-aria/utils"; +import {HTMLBanyuProps} from "@jala-banyu/system"; + +import {useModalContext} from "./modal-context"; +import {scaleInOut} from "./modal-transition"; + +type KeysToOmit = "children" | "role"; + +export interface ModalContentProps extends AriaDialogProps, HTMLBanyuProps<"div", KeysToOmit> { + children: ReactNode | ((onClose: () => void) => ReactNode); +} + +const ModalContent = forwardRef<"div", ModalContentProps, KeysToOmit>((props, _) => { + const {as, children, role = "dialog", ...otherProps} = props; + + const { + Component: DialogComponent, + domRef, + slots, + isOpen, + classNames, + motionProps, + backdrop, + closeButton, + hideCloseButton, + disableAnimation, + shouldBlockScroll, + getDialogProps, + getBackdropProps, + getCloseButtonProps, + onClose, + } = useModalContext(); + + const Component = as || DialogComponent || "div"; + + const {dialogProps} = useDialog( + { + role, + }, + domRef, + ); + + const closeButtonContent = isValidElement(closeButton) ? ( + cloneElement(closeButton, getCloseButtonProps()) + ) : ( + + ); + + const content = ( + + + {!hideCloseButton && closeButtonContent} + {typeof children === "function" ? children(onClose) : children} + + + ); + + const backdropContent = useMemo(() => { + if (backdrop === "transparent") { + return null; + } + + if (disableAnimation) { + return
; + } + + return ( + )} + /> + ); + }, [backdrop, disableAnimation, getBackdropProps]); + + return ( +
+ {backdropContent} + + {disableAnimation ? ( +
{content}
+ ) : ( + + {content} + + )} +
+
+ ); +}); + +ModalContent.displayName = "Banyu.ModalContent"; + +export default ModalContent; diff --git a/packages/components/modal/src/modal-context.ts b/packages/components/modal/src/modal-context.ts new file mode 100644 index 00000000..3e8bce12 --- /dev/null +++ b/packages/components/modal/src/modal-context.ts @@ -0,0 +1,9 @@ +import {createContext} from "@jala-banyu/react-utils"; + +import {UseModalReturn} from "./use-modal"; + +export const [ModalProvider, useModalContext] = createContext({ + name: "ModalContext", + errorMessage: + "useModalContext: `context` is undefined. Seems you forgot to wrap all popover components within ``", +}); diff --git a/packages/components/modal/src/modal-footer.tsx b/packages/components/modal/src/modal-footer.tsx new file mode 100644 index 00000000..429fec45 --- /dev/null +++ b/packages/components/modal/src/modal-footer.tsx @@ -0,0 +1,31 @@ +import {forwardRef, HTMLBanyuProps} from "@jala-banyu/system"; +import {useDOMRef} from "@jala-banyu/react-utils"; +import {clsx} from "@jala-banyu/shared-utils"; + +import {useModalContext} from "./modal-context"; + +export interface ModalFooterProps extends HTMLBanyuProps<"footer"> {} + +const ModalFooter = forwardRef<"footer", ModalFooterProps>((props, ref) => { + const {as, children, className, ...otherProps} = props; + + const {slots, classNames} = useModalContext(); + + const domRef = useDOMRef(ref); + + const Component = as || "footer"; + + return ( + + {children} + + ); +}); + +ModalFooter.displayName = "Banyu.ModalFooter"; + +export default ModalFooter; diff --git a/packages/components/modal/src/modal-header.tsx b/packages/components/modal/src/modal-header.tsx new file mode 100644 index 00000000..58918b3d --- /dev/null +++ b/packages/components/modal/src/modal-header.tsx @@ -0,0 +1,43 @@ +import {useEffect} from "react"; +import {forwardRef, HTMLBanyuProps} from "@jala-banyu/system"; +import {useDOMRef} from "@jala-banyu/react-utils"; +import {clsx} from "@jala-banyu/shared-utils"; + +import {useModalContext} from "./modal-context"; + +export interface ModalHeaderProps extends HTMLBanyuProps<"header"> {} + +const ModalHeader = forwardRef<"header", ModalHeaderProps>((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 ( + + {children} + + ); +}); + +ModalHeader.displayName = "Banyu.ModalHeader"; + +export default ModalHeader; diff --git a/packages/components/modal/src/modal-transition.ts b/packages/components/modal/src/modal-transition.ts new file mode 100644 index 00000000..76e95596 --- /dev/null +++ b/packages/components/modal/src/modal-transition.ts @@ -0,0 +1,33 @@ +import {TRANSITION_EASINGS} from "@jala-banyu/framer-transitions"; + +export const scaleInOut = { + enter: { + scale: "var(--scale-enter)", + y: "var(--slide-enter))", + opacity: 1, + transition: { + scale: { + duration: 0.4, + ease: TRANSITION_EASINGS.ease, + }, + opacity: { + duration: 0.4, + ease: TRANSITION_EASINGS.ease, + }, + y: { + type: "spring", + bounce: 0, + duration: 0.6, + }, + }, + }, + exit: { + scale: "var(--scale-exit)", + y: "var(--slide-exit)", + opacity: 0, + transition: { + duration: 0.3, + ease: TRANSITION_EASINGS.ease, + }, + }, +}; diff --git a/packages/components/modal/src/modal.tsx b/packages/components/modal/src/modal.tsx new file mode 100644 index 00000000..5b74c365 --- /dev/null +++ b/packages/components/modal/src/modal.tsx @@ -0,0 +1,35 @@ +import {ReactNode} from "react"; +import {AnimatePresence} from "framer-motion"; +import {Overlay} from "@react-aria/overlays"; +import {forwardRef} from "@jala-banyu/system"; + +import {UseModalProps, useModal} from "./use-modal"; +import {ModalProvider} from "./modal-context"; + +export interface ModalProps extends UseModalProps { + /** + * The content of the modal. Usually the ModalContent + */ + children: ReactNode; +} + +const Modal = forwardRef<"div", ModalProps>((props, ref) => { + const {children, ...otherProps} = props; + const context = useModal({...otherProps, ref}); + + const overlay = {children}; + + return ( + + {context.disableAnimation && context.isOpen ? ( + overlay + ) : ( + {context.isOpen ? overlay : null} + )} + + ); +}); + +Modal.displayName = "Banyu.Modal"; + +export default Modal; diff --git a/packages/components/modal/src/use-modal.ts b/packages/components/modal/src/use-modal.ts new file mode 100644 index 00000000..b230de6e --- /dev/null +++ b/packages/components/modal/src/use-modal.ts @@ -0,0 +1,203 @@ +import type {ModalVariantProps, SlotsToClasses, ModalSlots} from "@jala-banyu/theme"; +import type {HTMLMotionProps} from "framer-motion"; + +import {AriaModalOverlayProps} from "@react-aria/overlays"; +import {useAriaModalOverlay} from "@jala-banyu/use-aria-modal-overlay"; +import {useCallback, useId, useRef, useState, useMemo, ReactNode} from "react"; +import {modal} from "@jala-banyu/theme"; +import {HTMLBanyuProps, mapPropsVariants, PropGetter} from "@jala-banyu/system"; +import {useAriaButton} from "@jala-banyu/use-aria-button"; +import {useFocusRing} from "@react-aria/focus"; +import {clsx, dataAttr} from "@jala-banyu/shared-utils"; +import {ReactRef, useDOMRef} from "@jala-banyu/react-utils"; +import {useOverlayTriggerState} from "@react-stately/overlays"; +import {OverlayTriggerProps} from "@react-stately/overlays"; +import {mergeRefs, mergeProps} from "@react-aria/utils"; + +interface Props extends HTMLBanyuProps<"section"> { + /** + * Ref to the DOM node. + */ + ref?: ReactRef; + /** + * The props to modify the framer motion animation. Use the `variants` API to create your own animation. + */ + motionProps?: HTMLMotionProps<"section">; + /** + * Determines whether to hide the modal close button. + * @default false + */ + hideCloseButton?: boolean; + /** + * Custom modal close button element. + */ + closeButton?: ReactNode; + /** + * Whether the animation should be disabled. + * @default false + */ + disableAnimation?: boolean; + /** + * The container element in which the overlay portal will be placed. + * @default document.body + */ + portalContainer?: Element; + /** + * Whether the scroll should be blocked when the modal is open. + * @default true + */ + shouldBlockScroll?: 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. + * + * @example + * ```ts + * + * ``` + */ + classNames?: SlotsToClasses; +} + +export type UseModalProps = Props & OverlayTriggerProps & AriaModalOverlayProps & ModalVariantProps; + +export function useModal(originalProps: UseModalProps) { + const [props, variantProps] = mapPropsVariants(originalProps, modal.variantKeys); + + const { + ref, + as, + className, + classNames, + disableAnimation = false, + isOpen, + defaultOpen, + onOpenChange, + motionProps, + closeButton, + isDismissable = true, + hideCloseButton = false, + shouldBlockScroll = true, + portalContainer, + isKeyboardDismissDisabled = false, + onClose, + ...otherProps + } = props; + + const Component = as || "section"; + + const domRef = useDOMRef(ref); + const closeButtonRef = useRef(null); + + const [headerMounted, setHeaderMounted] = useState(false); + const [bodyMounted, setBodyMounted] = useState(false); + + const dialogId = useId(); + const headerId = useId(); + const bodyId = useId(); + + const state = useOverlayTriggerState({ + isOpen, + defaultOpen, + onOpenChange: (isOpen) => { + onOpenChange?.(isOpen); + if (!isOpen) { + onClose?.(); + } + }, + }); + + const {modalProps, underlayProps} = useAriaModalOverlay( + { + isDismissable, + isKeyboardDismissDisabled, + }, + state, + domRef, + ); + + const {buttonProps: closeButtonProps} = useAriaButton({onPress: state.close}, closeButtonRef); + const {isFocusVisible: isCloseButtonFocusVisible, focusProps: closeButtonFocusProps} = + useFocusRing(); + + const baseStyles = clsx(classNames?.base, className); + + const slots = useMemo( + () => + modal({ + ...variantProps, + }), + [...Object.values(variantProps)], + ); + + const getDialogProps: PropGetter = (props = {}, ref = null) => ({ + ref: mergeRefs(ref, domRef), + ...mergeProps(modalProps, otherProps, props), + className: slots.base({class: clsx(baseStyles, props.className)}), + id: dialogId, + "data-open": dataAttr(state.isOpen), + "data-dismissable": dataAttr(isDismissable), + "aria-modal": dataAttr(true), + "aria-labelledby": headerMounted ? headerId : undefined, + "aria-describedby": bodyMounted ? bodyId : undefined, + }); + + const getBackdropProps = useCallback( + (props = {}) => ({ + className: slots.backdrop({class: classNames?.backdrop}), + onClick: () => state.close(), + ...underlayProps, + ...props, + }), + [slots, classNames, underlayProps], + ); + + const getCloseButtonProps: PropGetter = () => { + return { + role: "button", + tabIndex: 0, + "aria-label": "Close", + "data-focus-visible": dataAttr(isCloseButtonFocusVisible), + className: slots.closeButton({class: classNames?.closeButton}), + ...mergeProps(closeButtonProps, closeButtonFocusProps), + }; + }; + + return { + Component, + slots, + domRef, + headerId, + bodyId, + motionProps, + classNames, + isDismissable, + closeButton, + hideCloseButton, + portalContainer, + shouldBlockScroll, + backdrop: originalProps.backdrop ?? "opaque", + isOpen: state.isOpen, + onClose: state.close, + disableAnimation, + setBodyMounted, + setHeaderMounted, + getDialogProps, + getBackdropProps, + getCloseButtonProps, + }; +} + +export type UseModalReturn = ReturnType; diff --git a/packages/components/modal/stories/modal.stories.tsx b/packages/components/modal/stories/modal.stories.tsx new file mode 100644 index 00000000..4f03648a --- /dev/null +++ b/packages/components/modal/stories/modal.stories.tsx @@ -0,0 +1,364 @@ +/* eslint-disable jsx-a11y/anchor-is-valid */ +/* eslint-disable jsx-a11y/no-autofocus */ +import React from "react"; +import {Meta} from "@storybook/react"; +import {modal} from "@jala-banyu/theme"; +import {Button} from "@jala-banyu/button"; +import {Input} from "@jala-banyu/input"; +import {CopyIcon} from "@jala-banyu/shared-icons"; +import Lorem from "react-lorem-component"; + +import { + Modal, + ModalContent, + ModalHeader, + ModalBody, + ModalFooter, + ModalProps, + useDisclosure, +} from "../src"; + +export default { + title: "Components/Modal", + component: Modal, + argTypes: { + size: { + control: { + type: "select", + }, + options: ["sm", "lg", "full"], + }, + radius: { + control: { + type: "select", + }, + options: ["none", "sm", "md", "lg", "xl"], + }, + backdrop: { + control: { + type: "select", + }, + options: ["transparent", "blur", "opaque"], + }, + disableAnimation: { + control: { + type: "boolean", + }, + }, + isDismissable: { + control: { + type: "boolean", + }, + }, + isKeyboardDismissDisabled: { + control: { + type: "boolean", + }, + }, + children: { + control: { + disable: true, + }, + }, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} as Meta; + +const defaultProps = { + ...modal.defaultVariants, + disableAnimation: false, + isDismissable: true, + isKeyboardDismissDisabled: false, +}; + +const content = ( + + {(onClose) => ( + <> + + Blog post published + + +
+ + This blog post has been published. Team members will be able to edit this post and + republish changes. + +
+
+ + + + + + )} +
+); + +const Template = (args: ModalProps) => { + const {isOpen, onOpen, onOpenChange} = useDisclosure({defaultOpen: args.defaultOpen}); + + return ( + <> + + + {content} + + + ); +}; + +const InsideScrollTemplate = (args: ModalProps) => { + const {isOpen, onOpen, onOpenChange} = useDisclosure(); + + return ( + <> + + + + +

Modal Title

+
+ + + +
+
+ + ); +}; + +const OutsideScrollTemplate = (args: ModalProps) => { + const {isOpen, onOpen, onClose, onOpenChange} = useDisclosure(); + + return ( + <> + + + + +

Modal Title

+
+ + + + + + +
+
+ + ); +}; +const OpenChangeTemplate = (args: ModalProps) => { + const {isOpen, onOpen, onClose, onOpenChange} = useDisclosure(); + + return ( +
+ + + + +

Modal Title

+
+ + + + + + +
+
+

isOpen: {isOpen ? "true" : "false"}

+
+ ); +}; + +const WithDividerTemplate = (args: ModalProps) => { + const {isOpen, onOpen, onClose, onOpenChange} = useDisclosure(); + + return ( +
+ + + + +

Invite your team

+ + You’ve created a new project! Invite colleagues to collaborate on this project. + +
+ +
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + + +
+ +
+ + + + +
+
+
+ ); +}; + +export const Default = { + render: Template, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/file/T0TUGURgVGElV6MtU2EPYU/%5BJDS%5D-Design-System---Banyu-1.0?node-id=3616%3A214326&mode=dev", + }, + }, + args: { + ...defaultProps, + }, +}; +export const WithDivider = { + render: WithDividerTemplate, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/file/T0TUGURgVGElV6MtU2EPYU/%5BJDS%5D-Design-System---Banyu-1.0?node-id=3616%3A214326&mode=dev", + }, + }, + args: { + ...defaultProps, + isDismissable: false, + }, +}; + +export const DefaultOpen = { + render: Template, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/file/T0TUGURgVGElV6MtU2EPYU/%5BJDS%5D-Design-System---Banyu-1.0?node-id=3616%3A214326&mode=dev", + }, + }, + args: { + ...defaultProps, + defaultOpen: true, + }, +}; + +export const OpenChange = { + render: OpenChangeTemplate, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/file/T0TUGURgVGElV6MtU2EPYU/%5BJDS%5D-Design-System---Banyu-1.0?node-id=3616%3A214326&mode=dev", + }, + }, + args: { + ...defaultProps, + scrollBehavior: "inside", + }, +}; + +export const InsideScroll = { + render: InsideScrollTemplate, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/file/T0TUGURgVGElV6MtU2EPYU/%5BJDS%5D-Design-System---Banyu-1.0?node-id=3616%3A214326&mode=dev", + }, + }, + args: { + ...defaultProps, + scrollBehavior: "inside", + }, +}; + +export const OutsideScroll = { + render: OutsideScrollTemplate, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/file/T0TUGURgVGElV6MtU2EPYU/%5BJDS%5D-Design-System---Banyu-1.0?node-id=3616%3A214326&mode=dev", + }, + }, + args: { + ...defaultProps, + }, +}; + +export const DisableAnimation = { + render: Template, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/file/T0TUGURgVGElV6MtU2EPYU/%5BJDS%5D-Design-System---Banyu-1.0?node-id=3616%3A214326&mode=dev", + }, + }, + args: { + ...defaultProps, + disableAnimation: true, + }, +}; + +export const CustomMotion = { + render: Template, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/file/T0TUGURgVGElV6MtU2EPYU/%5BJDS%5D-Design-System---Banyu-1.0?node-id=3616%3A214326&mode=dev", + }, + }, + args: { + ...defaultProps, + motionProps: { + variants: { + enter: { + opacity: 1, + y: 0, + duration: 0.3, + }, + exit: { + y: 20, + opacity: 0, + duration: 0.3, + }, + }, + }, + }, +}; diff --git a/packages/components/modal/tsconfig.json b/packages/components/modal/tsconfig.json new file mode 100644 index 00000000..5d012f6e --- /dev/null +++ b/packages/components/modal/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "tailwind-variants": ["../../../node_modules/tailwind-variants"] + }, + }, + "include": ["src", "index.ts"] +} diff --git a/packages/components/modal/tsup.config.ts b/packages/components/modal/tsup.config.ts new file mode 100644 index 00000000..3e2bcff6 --- /dev/null +++ b/packages/components/modal/tsup.config.ts @@ -0,0 +1,8 @@ +import {defineConfig} from "tsup"; + +export default defineConfig({ + clean: true, + target: "es2019", + format: ["cjs", "esm"], + banner: {js: '"use client";'}, +}); diff --git a/packages/core/theme/src/components/index.ts b/packages/core/theme/src/components/index.ts index 470bce3f..245edf8b 100644 --- a/packages/core/theme/src/components/index.ts +++ b/packages/core/theme/src/components/index.ts @@ -20,7 +20,7 @@ export * from "./accordion"; export * from "./input"; export * from "./dropdown"; export * from "./image"; -// export * from "./modal"; +export * from "./modal"; // export * from "./navbar"; // export * from "./table"; export * from "./spacer"; diff --git a/packages/core/theme/src/components/modal.ts b/packages/core/theme/src/components/modal.ts new file mode 100644 index 00000000..518e2c56 --- /dev/null +++ b/packages/core/theme/src/components/modal.ts @@ -0,0 +1,189 @@ +import type {VariantProps} from "tailwind-variants"; + +import {tv} from "../utils/tv"; +import {dataFocusVisibleClasses} from "../utils"; + +/** + * Modal **Tailwind Variants** component + * + * @example + * ```js + * const {base, backdrop, header, body, footer} = modal({...}) + * + *
+ * + *
+ *
+ *
Header
+ *
Body
+ *
Footer
+ *
+ *
+ * ``` + */ +const modal = tv({ + slots: { + wrapper: [ + "flex", + "w-screen", + "h-[100dvh]", + "fixed", + "inset-0", + "z-50", + "overflow-x-auto", + "justify-center", + // mobile animation vars + "[--scale-enter:100%]", + "[--scale-exit:100%]", + "[--slide-enter:0px]", + "[--slide-exit:80px]", + // tablet/desktop animation vars + "sm:[--scale-enter:100%]", + "sm:[--scale-exit:103%]", + "sm:[--slide-enter:0px]", + "sm:[--slide-exit:0px]", + ], + base: [ + "flex", + "flex-col", + "relative", + "bg-white", + "z-50", + "w-full", + "box-border", + "bg-white", + "outline-none", + "mx-1", + "my-1", + "sm:mx-6", + "sm:my-16", + "gap-[4px]", + "pb-6", + ], + backdrop: "z-50", + header: "flex pt-6 px-6 flex-initial", + body: "flex flex-1 flex-col gap-3 px-6 font-normal leading-5", + footer: "flex flex-row gap-2 px-6 pt-unit-6 gap-3", + closeButton: [ + "absolute", + "appearance-none", + "outline-none", + "select-none", + "top-4", + "right-4", + "p-1", + "text-foreground-500", + "rounded-full", + "hover:bg-default-100", + "active:bg-default-200", + "tap-highlight-transparent", + "bg-neutral-100", + "text-xl", + // focus ring + ...dataFocusVisibleClasses, + ], + }, + variants: { + size: { + sm: { + base: "max-w-[400px]", + }, + lg: { + base: "max-w-[640px]", + }, + full: { + base: "sm:mx-0 sm:my-0 max-w-[95dvw] m-8 h-[90dvh]", + }, + }, + radius: { + none: {base: "rounded-none"}, + sm: {base: "rounded-sm"}, + md: {base: "rounded-md"}, + lg: {base: "rounded-lg"}, + xl: {base: "rounded-xl"}, + }, + placement: { + auto: { + wrapper: "items-end sm:items-center", + }, + center: { + wrapper: "items-center sm:items-center", + }, + top: { + wrapper: "items-start sm:items-start", + }, + "top-center": { + wrapper: "items-start sm:items-center", + }, + bottom: { + wrapper: "items-end sm:items-end", + }, + "bottom-center": { + wrapper: "items-end sm:items-center", + }, + }, + shadow: { + sm: { + base: "shadow-small", + }, + md: { + base: "shadow-medium", + }, + lg: { + base: "shadow-large", + }, + }, + backdrop: { + transparent: { + backdrop: "hidden", + }, + opaque: { + backdrop: "bg-overlay/50 backdrop-opacity-disabled", + }, + blur: { + backdrop: "backdrop-blur-md backdrop-saturate-150 bg-overlay/30", + }, + }, + scrollBehavior: { + normal: { + base: "overflow-y-hidden", + }, + inside: { + base: "max-h-[calc(100%_-_7.5rem)]", + body: "overflow-y-auto", + }, + outside: { + wrapper: "items-start sm:items-start overflow-y-auto", + base: "my-16", + }, + }, + isDismissable: { + true: "", + false: { + closeButton: "hidden", + }, + }, + }, + defaultVariants: { + size: "sm", + radius: "lg", + shadow: "sm", + placement: "auto", + backdrop: "opaque", + scrollBehavior: "normal", + }, + compoundVariants: [ + // backdrop (opaque/blur) + { + backdrop: ["opaque", "blur"], + class: { + backdrop: "w-screen h-screen fixed inset-0", + }, + }, + ], +}); + +export type ModalVariantProps = VariantProps; +export type ModalSlots = keyof ReturnType; + +export {modal}; diff --git a/packages/hooks/use-aria-modal-overlay/README.md b/packages/hooks/use-aria-modal-overlay/README.md new file mode 100644 index 00000000..cc379b9e --- /dev/null +++ b/packages/hooks/use-aria-modal-overlay/README.md @@ -0,0 +1,24 @@ +# @jala-banyu/use-aria-modal-overlay + +A Quick description of the component + +> This is an internal utility, not intended for public usage. + +## Installation + +```sh +yarn add @jala-banyu/use-aria-modal-overlay +# or +npm i @jala-banyu/use-aria-modal-overlay +``` + +## Contribution + +Yes please! See the +[contributing guidelines](https://github.com/Atnic/banyu/blob/master/CONTRIBUTING.md) +for details. + +## Licence + +This project is licensed under the terms of the +[MIT license](https://github.com/Atnic/banyu/blob/master/LICENSE). diff --git a/packages/hooks/use-aria-modal-overlay/package.json b/packages/hooks/use-aria-modal-overlay/package.json new file mode 100644 index 00000000..9689de44 --- /dev/null +++ b/packages/hooks/use-aria-modal-overlay/package.json @@ -0,0 +1,61 @@ +{ + "name": "@jala-banyu/use-aria-modal-overlay", + "version": "0.0.0", + "description": "A custom implementation of react aria modal overlay, this removes the prevent scroll", + "keywords": [ + "use-aria-modal-overlay" + ], + "author": "Muhammad Amien ", + "homepage": "#", + "license": "MIT", + "main": "src/index.ts", + "sideEffects": false, + "files": [ + "dist" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Atnic/banyu.git", + "directory": "packages/hooks/use-aria-modal-overlay" + }, + "bugs": { + "url": "https://github.com/Atnic/banyu/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" + }, + "dependencies": { + "@react-aria/overlays": "^3.18.1", + "@react-stately/overlays": "^3.6.3", + "@react-types/shared": "^3.21.0", + "@react-aria/utils": "^3.21.1" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "devDependencies": { + "clean-package": "2.2.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "clean-package": "../../../clean-package.config.json", + "tsup": { + "clean": true, + "target": "es2019", + "format": [ + "cjs", + "esm" + ] + } +} + diff --git a/packages/hooks/use-aria-modal-overlay/src/index.ts b/packages/hooks/use-aria-modal-overlay/src/index.ts new file mode 100644 index 00000000..d3e0404f --- /dev/null +++ b/packages/hooks/use-aria-modal-overlay/src/index.ts @@ -0,0 +1,42 @@ +import { + ariaHideOutside, + AriaModalOverlayProps, + ModalOverlayAria, + useOverlay, + useOverlayFocusContain, +} from "@react-aria/overlays"; +import {mergeProps} from "@react-aria/utils"; +import {OverlayTriggerState} from "@react-stately/overlays"; +import {RefObject, useEffect} from "react"; + +export interface UseAriaModalOverlayProps extends AriaModalOverlayProps {} + +export function useAriaModalOverlay( + props: UseAriaModalOverlayProps = {}, + state: OverlayTriggerState, + ref: RefObject, +): ModalOverlayAria { + let {overlayProps, underlayProps} = useOverlay( + { + ...props, + isOpen: state.isOpen, + onClose: state.close, + }, + ref, + ); + + useOverlayFocusContain(); + + useEffect(() => { + if (state.isOpen && ref.current) { + return ariaHideOutside([ref.current]); + } + }, [state.isOpen, ref]); + + return { + modalProps: mergeProps(overlayProps), + underlayProps, + }; +} + +export type UseAriaModalOverlayReturn = ReturnType; diff --git a/packages/hooks/use-aria-modal-overlay/tsconfig.json b/packages/hooks/use-aria-modal-overlay/tsconfig.json new file mode 100644 index 00000000..46e3b466 --- /dev/null +++ b/packages/hooks/use-aria-modal-overlay/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["src", "index.ts"] +} diff --git a/packages/hooks/use-callback-ref/README.md b/packages/hooks/use-callback-ref/README.md new file mode 100644 index 00000000..de92cac8 --- /dev/null +++ b/packages/hooks/use-callback-ref/README.md @@ -0,0 +1,24 @@ +# @jala-banyu/use-callback-ref + +A Quick description of the component + +> This is an internal utility, not intended for public usage. + +## Installation + +```sh +yarn add @jala-banyu/use-callback-ref +# or +npm i @jala-banyu/use-callback-ref +``` + +## Contribution + +Yes please! See the +[contributing guidelines](https://github.com/Atnic/banyu/blob/master/CONTRIBUTING.md) +for details. + +## Licence + +This project is licensed under the terms of the +[MIT license](https://github.com/Atnic/banyu/blob/master/LICENSE). diff --git a/packages/hooks/use-callback-ref/package.json b/packages/hooks/use-callback-ref/package.json new file mode 100644 index 00000000..2f1601bb --- /dev/null +++ b/packages/hooks/use-callback-ref/package.json @@ -0,0 +1,55 @@ +{ + "name": "@jala-banyu/use-callback-ref", + "version": "0.0.0", + "description": "React hook to persist any value between renders, but keeps it up-to-date if it changes.", + "keywords": [ + "use-callback-ref" + ], + "author": "Muhammad Amien ", + "homepage": "#", + "license": "MIT", + "main": "src/index.ts", + "sideEffects": false, + "files": [ + "dist" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Atnic/banyu.git", + "directory": "packages/hooks/use-callback-ref" + }, + "bugs": { + "url": "https://github.com/Atnic/banyu/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" + }, + "dependencies": { + "@jala-banyu/use-safe-layout-effect": "workspace:*" + }, + "peerDependencies": { + "react": ">=18" + }, + "devDependencies": { + "clean-package": "2.2.0", + "react": "^18.0.0" + }, + "clean-package": "../../../clean-package.config.json", + "tsup": { + "clean": true, + "target": "es2019", + "format": [ + "cjs", + "esm" + ] + } +} diff --git a/packages/hooks/use-callback-ref/src/index.ts b/packages/hooks/use-callback-ref/src/index.ts new file mode 100644 index 00000000..343f1047 --- /dev/null +++ b/packages/hooks/use-callback-ref/src/index.ts @@ -0,0 +1,27 @@ +/** + * Part of this code is taken from @chakra-ui/system ❤️ + */ + +import {useCallback, useRef} from "react"; +import {useSafeLayoutEffect} from "@jala-banyu/use-safe-layout-effect"; + +/** + * React hook to persist any value between renders, + * but keeps it up-to-date if it changes. + * + * @param fn the function to persist + * @param deps the function dependency list + */ +export function useCallbackRef any>( + fn: T | undefined, + deps: React.DependencyList = [], +): T { + const ref = useRef(fn); + + useSafeLayoutEffect(() => { + ref.current = fn; + }); + + // eslint-disable-next-line react-hooks/exhaustive-deps + return useCallback(((...args) => ref.current?.(...args)) as T, deps); +} diff --git a/packages/hooks/use-callback-ref/tsconfig.json b/packages/hooks/use-callback-ref/tsconfig.json new file mode 100644 index 00000000..46e3b466 --- /dev/null +++ b/packages/hooks/use-callback-ref/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["src", "index.ts"] +} diff --git a/packages/hooks/use-clipboard/README.md b/packages/hooks/use-clipboard/README.md new file mode 100644 index 00000000..a6b7307a --- /dev/null +++ b/packages/hooks/use-clipboard/README.md @@ -0,0 +1,24 @@ +# @nextui-org/use-clipboard + +A Quick description of the component + +> This is an internal utility, not intended for public usage. + +## Installation + +```sh +yarn add @nextui-org/use-clipboard +# or +npm i @nextui-org/use-clipboard +``` + +## 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). diff --git a/packages/hooks/use-clipboard/__tests__/clipboard.test.tsx b/packages/hooks/use-clipboard/__tests__/clipboard.test.tsx new file mode 100644 index 00000000..bb5fe4c8 --- /dev/null +++ b/packages/hooks/use-clipboard/__tests__/clipboard.test.tsx @@ -0,0 +1,38 @@ +import {renderHook, act} from "@testing-library/react-hooks"; + +import {useClipboard} from "../src"; + +describe("UseClipboard", () => { + beforeAll(() => { + jest.useFakeTimers(); + // navigator.clipboard.writeText mock + Object.assign(navigator, { + clipboard: { + writeText: (data: string) => + new Promise((res, rej) => { + try { + res(data); + } catch (error) { + rej(error); + } + }), + }, + }); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it("should copy text to clipboard", () => { + jest.spyOn(navigator.clipboard, "writeText"); + + const {result} = renderHook(() => useClipboard({timeout: 0})); + + act(() => { + result.current.copy("test"); + }); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith("test"); + }); +}); diff --git a/packages/hooks/use-clipboard/package.json b/packages/hooks/use-clipboard/package.json new file mode 100644 index 00000000..3cb17dd7 --- /dev/null +++ b/packages/hooks/use-clipboard/package.json @@ -0,0 +1,52 @@ +{ + "name": "@jala-banyu/use-clipboard", + "version": "0.0.0", + "description": "Wrapper around navigator.clipboard with feedback timeout", + "keywords": [ + "banyu-clipboard" + ], + "author": "Muhammad Amien ", + "homepage": "#", + "license": "MIT", + "main": "src/index.ts", + "sideEffects": false, + "files": [ + "dist" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Atnic/banyu.git", + "directory": "packages/hooks/use-clipboard" + }, + "bugs": { + "url": "https://github.com/Atnic/banyu/issues" + }, + "scripts": { + "build": "tsup src --dts", + "dev": "yarn build:fast -- --watch", + "clean": "rimraf dist .turbo", + "typecheck": "tsc --noEmit", + "build:fast": "tsup src", + "prepack": "clean-package", + "postpack": "clean-package restore" + }, + "peerDependencies": { + "react": ">=18" + }, + "devDependencies": { + "clean-package": "2.2.0", + "react": "^18.0.0" + }, + "clean-package": "../../../clean-package.config.json", + "tsup": { + "clean": true, + "target": "es2019", + "format": [ + "cjs", + "esm" + ] + } +} diff --git a/packages/hooks/use-clipboard/src/index.ts b/packages/hooks/use-clipboard/src/index.ts new file mode 100644 index 00000000..cc9d30b0 --- /dev/null +++ b/packages/hooks/use-clipboard/src/index.ts @@ -0,0 +1,53 @@ +import {useState} from "react"; + +export interface UseClipboardProps { + /** + * The time in milliseconds to wait before resetting the clipboard. + * @default 2000 + */ + timeout?: number; +} + +/** + * Copies the given text to the clipboard. + * @param {number} timeout - timeout in ms, default 2000 + * @returns {copy, copied, error, reset} - copy function, copied state, error state, reset function + */ +export function useClipboard({timeout = 2000}: UseClipboardProps = {}) { + const [error, setError] = useState(null); + const [copied, setCopied] = useState(false); + const [copyTimeout, setCopyTimeout] = useState | null>(null); + + const onClearTimeout = () => { + if (copyTimeout) { + clearTimeout(copyTimeout); + } + }; + + const handleCopyResult = (value: boolean) => { + onClearTimeout(); + setCopyTimeout(setTimeout(() => setCopied(false), timeout)); + setCopied(value); + }; + + const copy = (valueToCopy: any) => { + if ("clipboard" in navigator) { + navigator.clipboard + .writeText(valueToCopy) + .then(() => handleCopyResult(true)) + .catch((err) => setError(err)); + } else { + setError(new Error("useClipboard: navigator.clipboard is not supported")); + } + }; + + const reset = () => { + setCopied(false); + setError(null); + onClearTimeout(); + }; + + return {copy, reset, error, copied}; +} + +export type UseClipboardReturn = ReturnType; diff --git a/packages/hooks/use-clipboard/tsconfig.json b/packages/hooks/use-clipboard/tsconfig.json new file mode 100644 index 00000000..46e3b466 --- /dev/null +++ b/packages/hooks/use-clipboard/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["src", "index.ts"] +} diff --git a/packages/hooks/use-disclosure/README.md b/packages/hooks/use-disclosure/README.md new file mode 100644 index 00000000..d5b9bfe5 --- /dev/null +++ b/packages/hooks/use-disclosure/README.md @@ -0,0 +1,24 @@ +# @jala-banyu/use-disclosure + +A Quick description of the component + +> This is an internal utility, not intended for public usage. + +## Installation + +```sh +yarn add @jala-banyu/use-disclosure +# or +npm i @jala-banyu/use-disclosure +``` + +## Contribution + +Yes please! See the +[contributing guidelines](https://github.com/Atnic/banyu/blob/master/CONTRIBUTING.md) +for details. + +## Licence + +This project is licensed under the terms of the +[MIT license](https://github.com/Atnic/banyu/blob/master/LICENSE). diff --git a/packages/hooks/use-disclosure/package.json b/packages/hooks/use-disclosure/package.json new file mode 100644 index 00000000..67d782df --- /dev/null +++ b/packages/hooks/use-disclosure/package.json @@ -0,0 +1,57 @@ +{ + "name": "@jala-banyu/use-disclosure", + "version": "0.0.0", + "description": "", + "keywords": [ + "use-disclosure" + ], + "author": "Muhammad Amien ", + "homepage": "#", + "license": "MIT", + "main": "src/index.ts", + "sideEffects": false, + "files": [ + "dist" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Atnic/banyu.git", + "directory": "packages/hooks/use-disclosure" + }, + "bugs": { + "url": "https://github.com/Atnic/banyu/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" + }, + "dependencies": { + "@jala-banyu/use-callback-ref": "workspace:*", + "@react-stately/utils": "^3.8.0", + "@react-aria/utils": "^3.21.1" + }, + "peerDependencies": { + "react": ">=18" + }, + "devDependencies": { + "clean-package": "2.2.0", + "react": "^18.0.0" + }, + "clean-package": "../../../clean-package.config.json", + "tsup": { + "clean": true, + "target": "es2019", + "format": [ + "cjs", + "esm" + ] + } +} diff --git a/packages/hooks/use-disclosure/src/index.ts b/packages/hooks/use-disclosure/src/index.ts new file mode 100644 index 00000000..c8cf18ab --- /dev/null +++ b/packages/hooks/use-disclosure/src/index.ts @@ -0,0 +1,73 @@ +import {chain} from "@react-aria/utils"; +import {useControlledState} from "@react-stately/utils"; +import {useCallbackRef} from "@jala-banyu/use-callback-ref"; +import {useCallback, useId} from "react"; + +export interface UseDisclosureProps { + isOpen?: boolean; + defaultOpen?: boolean; + onClose?(): void; + onOpen?(): void; + onChange?(isOpen: boolean | undefined): void; + id?: string; +} + +export function useDisclosure(props: UseDisclosureProps = {}) { + const { + id: idProp, + defaultOpen, + isOpen: isOpenProp, + onClose: onCloseProp, + onOpen: onOpenProp, + onChange = () => {}, + } = props; + + const onOpenPropCallbackRef = useCallbackRef(onOpenProp); + const onClosePropCallbackRef = useCallbackRef(onCloseProp); + const [isOpen, setIsOpen] = useControlledState(isOpenProp, defaultOpen || false, onChange); + + const reactId = useId(); + const id = idProp || reactId; + const isControlled = isOpenProp !== undefined; + + const onClose = useCallback(() => { + if (!isControlled) { + setIsOpen(false); + } + onClosePropCallbackRef?.(); + }, [isControlled, onClosePropCallbackRef]); + + const onOpen = useCallback(() => { + if (!isControlled) { + setIsOpen(true); + } + onOpenPropCallbackRef?.(); + }, [isControlled, onOpenPropCallbackRef]); + + const onOpenChange = useCallback(() => { + const action = isOpen ? onClose : onOpen; + + action(); + }, [isOpen, onOpen, onClose]); + + return { + isOpen: !!isOpen, + onOpen, + onClose, + onOpenChange, + isControlled, + getButtonProps: (props: any = {}) => ({ + ...props, + "aria-expanded": isOpen, + "aria-controls": id, + onClick: chain(props.onClick, onOpenChange), + }), + getDisclosureProps: (props: any = {}) => ({ + ...props, + hidden: !isOpen, + id, + }), + }; +} + +export type UseDisclosureReturn = ReturnType; diff --git a/packages/hooks/use-disclosure/tsconfig.json b/packages/hooks/use-disclosure/tsconfig.json new file mode 100644 index 00000000..46e3b466 --- /dev/null +++ b/packages/hooks/use-disclosure/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["src", "index.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d410ff34..c1b36788 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -962,6 +962,88 @@ importers: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + packages/components/modal: + dependencies: + '@jala-banyu/framer-transitions': + specifier: workspace:* + version: link:../../utilities/framer-transitions + '@jala-banyu/react-utils': + specifier: workspace:* + version: link:../../utilities/react-utils + '@jala-banyu/shared-icons': + specifier: workspace:* + version: link:../../utilities/shared-icons + '@jala-banyu/shared-utils': + specifier: workspace:* + version: link:../../utilities/shared-utils + '@jala-banyu/use-aria-button': + specifier: workspace:* + version: link:../../hooks/use-aria-button + '@jala-banyu/use-aria-modal-overlay': + specifier: workspace:* + version: link:../../hooks/use-aria-modal-overlay + '@jala-banyu/use-disclosure': + specifier: workspace:* + version: link:../../hooks/use-disclosure + '@react-aria/dialog': + specifier: ^3.5.7 + version: 3.5.12(react-dom@18.2.0)(react@18.2.0) + '@react-aria/focus': + specifier: ^3.14.3 + version: 3.16.2(react@18.2.0) + '@react-aria/interactions': + specifier: ^3.19.1 + version: 3.21.1(react@18.2.0) + '@react-aria/overlays': + specifier: ^3.18.1 + version: 3.21.1(react-dom@18.2.0)(react@18.2.0) + '@react-aria/utils': + specifier: ^3.21.1 + version: 3.23.2(react@18.2.0) + '@react-stately/overlays': + specifier: ^3.6.3 + version: 3.6.5(react@18.2.0) + '@react-types/overlays': + specifier: ^3.8.3 + version: 3.8.5(react@18.2.0) + react-remove-scroll: + specifier: ^2.5.6 + version: 2.5.7(@types/react@18.2.56)(react@18.2.0) + devDependencies: + '@jala-banyu/button': + specifier: workspace:* + version: link:../button + '@jala-banyu/checkbox': + specifier: workspace:* + version: link:../checkbox + '@jala-banyu/input': + specifier: workspace:* + version: link:../input + '@jala-banyu/link': + specifier: workspace:* + version: link:../link + '@jala-banyu/system': + specifier: workspace:* + version: link:../../core/system + '@jala-banyu/theme': + specifier: workspace:* + version: link:../../core/theme + clean-package: + specifier: 2.2.0 + version: 2.2.0 + framer-motion: + specifier: ^10.16.4 + version: 10.18.0(react-dom@18.2.0)(react@18.2.0) + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + react-lorem-component: + specifier: 0.13.0 + version: 0.13.0(react@18.2.0) + packages/components/popover: dependencies: '@jala-banyu/aria-utils': @@ -1671,6 +1753,31 @@ importers: specifier: ^18.2.0 version: 18.2.0 + packages/hooks/use-aria-modal-overlay: + dependencies: + '@react-aria/overlays': + specifier: ^3.18.1 + version: 3.21.1(react-dom@18.2.0)(react@18.2.0) + '@react-aria/utils': + specifier: ^3.21.1 + version: 3.23.2(react@18.2.0) + '@react-stately/overlays': + specifier: ^3.6.3 + version: 3.6.5(react@18.2.0) + '@react-types/shared': + specifier: ^3.21.0 + version: 3.22.1(react@18.2.0) + devDependencies: + clean-package: + specifier: 2.2.0 + version: 2.2.0 + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + packages/hooks/use-aria-press: dependencies: '@react-aria/interactions': @@ -1693,6 +1800,47 @@ importers: specifier: ^18.2.0 version: 18.2.0 + packages/hooks/use-callback-ref: + dependencies: + '@jala-banyu/use-safe-layout-effect': + specifier: workspace:* + version: link:../use-safe-layout-effect + devDependencies: + clean-package: + specifier: 2.2.0 + version: 2.2.0 + react: + specifier: ^18.2.0 + version: 18.2.0 + + packages/hooks/use-clipboard: + devDependencies: + clean-package: + specifier: 2.2.0 + version: 2.2.0 + react: + specifier: ^18.2.0 + version: 18.2.0 + + packages/hooks/use-disclosure: + dependencies: + '@jala-banyu/use-callback-ref': + specifier: workspace:* + version: link:../use-callback-ref + '@react-aria/utils': + specifier: ^3.21.1 + version: 3.23.2(react@18.2.0) + '@react-stately/utils': + specifier: ^3.8.0 + version: 3.9.1(react@18.2.0) + devDependencies: + clean-package: + specifier: 2.2.0 + version: 2.2.0 + react: + specifier: ^18.2.0 + version: 18.2.0 + packages/hooks/use-image: dependencies: '@jala-banyu/use-safe-layout-effect':