@myzbox/react-overlay 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +457 -0
- package/dist/index.d.ts +1 -0
- package/dist/react-overlay.css +1 -0
- package/dist/react-overlay.es.js +865 -0
- package/dist/react-overlay.umd.js +26 -0
- package/package.json +85 -0
- package/src/components/index.ts +11 -0
- package/src/components/modal/Modal.tsx +186 -0
- package/src/components/modal-variants/AlertModal.tsx +56 -0
- package/src/components/modal-variants/ConfirmModal.tsx +78 -0
- package/src/components/modal-variants/Drawer.tsx +51 -0
- package/src/components/popover/Popover.tsx +54 -0
- package/src/components/toast/Toast.tsx +61 -0
- package/src/components/toast/ToastProvider.tsx +71 -0
- package/src/components/toast/index.ts +3 -0
- package/src/components/toast/useToast.ts +48 -0
- package/src/components/tooltip/Tooltip.tsx +52 -0
- package/src/hooks/useClickOutside.ts +24 -0
- package/src/hooks/useDraggable.ts +75 -0
- package/src/hooks/useModal.ts +23 -0
- package/src/index.ts +8 -0
- package/src/styles/Modal.module.css +314 -0
- package/src/styles/ModalVariants.module.css +132 -0
- package/src/styles/Popover.module.css +70 -0
- package/src/styles/Toast.module.css +196 -0
- package/src/styles/Tooltip.module.css +50 -0
- package/src/styles/index.css +5 -0
- package/src/types/Modal.ts +100 -0
- package/src/types/Popover.ts +13 -0
- package/src/types/Toast.ts +19 -0
- package/src/types/Tooltip.ts +19 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
(function(b,u){typeof exports=="object"&&typeof module<"u"?u(exports,require("react"),require("react-dom")):typeof define=="function"&&define.amd?define(["exports","react","react-dom"],u):(b=typeof globalThis<"u"?globalThis:b||self,u(b.ReactOverlay={},b.React,b.ReactDOM))})(this,(function(b,u,me){"use strict";function pe(r){return r&&r.__esModule&&Object.prototype.hasOwnProperty.call(r,"default")?r.default:r}var Y={exports:{}},M={};/**
|
|
2
|
+
* @license React
|
|
3
|
+
* react-jsx-runtime.production.js
|
|
4
|
+
*
|
|
5
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
6
|
+
*
|
|
7
|
+
* This source code is licensed under the MIT license found in the
|
|
8
|
+
* LICENSE file in the root directory of this source tree.
|
|
9
|
+
*/var oe;function be(){if(oe)return M;oe=1;var r=Symbol.for("react.transitional.element"),a=Symbol.for("react.fragment");function s(l,n,t){var o=null;if(t!==void 0&&(o=""+t),n.key!==void 0&&(o=""+n.key),"key"in n){t={};for(var c in n)c!=="key"&&(t[c]=n[c])}else t=n;return n=t.ref,{$$typeof:r,type:l,key:o,ref:n!==void 0?n:null,props:t}}return M.Fragment=a,M.jsx=s,M.jsxs=s,M}var I={};/**
|
|
10
|
+
* @license React
|
|
11
|
+
* react-jsx-runtime.development.js
|
|
12
|
+
*
|
|
13
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
14
|
+
*
|
|
15
|
+
* This source code is licensed under the MIT license found in the
|
|
16
|
+
* LICENSE file in the root directory of this source tree.
|
|
17
|
+
*/var ne;function he(){return ne||(ne=1,process.env.NODE_ENV!=="production"&&(function(){function r(e){if(e==null)return null;if(typeof e=="function")return e.$$typeof===O?null:e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case C:return"Fragment";case B:return"Profiler";case K:return"StrictMode";case L:return"Suspense";case Q:return"SuspenseList";case w:return"Activity"}if(typeof e=="object")switch(typeof e.tag=="number"&&console.error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."),e.$$typeof){case Z:return"Portal";case W:return e.displayName||"Context";case F:return(e._context.displayName||"Context")+".Consumer";case U:var d=e.render;return e=e.displayName,e||(e=d.displayName||d.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case ee:return d=e.displayName||null,d!==null?d:r(e.type)||"Memo";case D:d=e._payload,e=e._init;try{return r(e(d))}catch{}}return null}function a(e){return""+e}function s(e){try{a(e);var d=!1}catch{d=!0}if(d){d=console;var p=d.error,h=typeof Symbol=="function"&&Symbol.toStringTag&&e[Symbol.toStringTag]||e.constructor.name||"Object";return p.call(d,"The provided key is an unsupported type %s. This value must be coerced to a string before using it here.",h),a(e)}}function l(e){if(e===C)return"<>";if(typeof e=="object"&&e!==null&&e.$$typeof===D)return"<...>";try{var d=r(e);return d?"<"+d+">":"<...>"}catch{return"<...>"}}function n(){var e=S.A;return e===null?null:e.getOwner()}function t(){return Error("react-stack-top-frame")}function o(e){if(P.call(e,"key")){var d=Object.getOwnPropertyDescriptor(e,"key").get;if(d&&d.isReactWarning)return!1}return e.key!==void 0}function c(e,d){function p(){ie||(ie=!0,console.error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)",d))}p.isReactWarning=!0,Object.defineProperty(e,"key",{get:p,configurable:!0})}function i(){var e=r(this.type);return ue[e]||(ue[e]=!0,console.error("Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release.")),e=this.props.ref,e!==void 0?e:null}function _(e,d,p,h,X,te){var v=p.ref;return e={$$typeof:$,type:e,key:d,props:p,_owner:h},(v!==void 0?v:null)!==null?Object.defineProperty(e,"ref",{enumerable:!1,get:i}):Object.defineProperty(e,"ref",{enumerable:!1,value:null}),e._store={},Object.defineProperty(e._store,"validated",{configurable:!1,enumerable:!1,writable:!0,value:0}),Object.defineProperty(e,"_debugInfo",{configurable:!1,enumerable:!1,writable:!0,value:null}),Object.defineProperty(e,"_debugStack",{configurable:!1,enumerable:!1,writable:!0,value:X}),Object.defineProperty(e,"_debugTask",{configurable:!1,enumerable:!1,writable:!0,value:te}),Object.freeze&&(Object.freeze(e.props),Object.freeze(e)),e}function m(e,d,p,h,X,te){var v=d.children;if(v!==void 0)if(h)if(z(v)){for(h=0;h<v.length;h++)g(v[h]);Object.freeze&&Object.freeze(v)}else console.error("React.jsx: Static children should always be an array. You are likely explicitly calling React.jsxs or React.jsxDEV. Use the Babel transform instead.");else g(v);if(P.call(d,"key")){v=r(e);var A=Object.keys(d).filter(function(Ce){return Ce!=="key"});h=0<A.length?"{key: someKey, "+A.join(": ..., ")+": ...}":"{key: someKey}",_e[v+h]||(A=0<A.length?"{"+A.join(": ..., ")+": ...}":"{}",console.error(`A props object containing a "key" prop is being spread into JSX:
|
|
18
|
+
let props = %s;
|
|
19
|
+
<%s {...props} />
|
|
20
|
+
React keys must be passed directly to JSX without using spread:
|
|
21
|
+
let props = %s;
|
|
22
|
+
<%s key={someKey} {...props} />`,h,v,A,v),_e[v+h]=!0)}if(v=null,p!==void 0&&(s(p),v=""+p),o(d)&&(s(d.key),v=""+d.key),"key"in d){p={};for(var re in d)re!=="key"&&(p[re]=d[re])}else p=d;return v&&c(p,typeof e=="function"?e.displayName||e.name||"Unknown":e),_(e,v,p,n(),X,te)}function g(e){E(e)?e._store&&(e._store.validated=1):typeof e=="object"&&e!==null&&e.$$typeof===D&&(e._payload.status==="fulfilled"?E(e._payload.value)&&e._payload.value._store&&(e._payload.value._store.validated=1):e._store&&(e._store.validated=1))}function E(e){return typeof e=="object"&&e!==null&&e.$$typeof===$}var k=u,$=Symbol.for("react.transitional.element"),Z=Symbol.for("react.portal"),C=Symbol.for("react.fragment"),K=Symbol.for("react.strict_mode"),B=Symbol.for("react.profiler"),F=Symbol.for("react.consumer"),W=Symbol.for("react.context"),U=Symbol.for("react.forward_ref"),L=Symbol.for("react.suspense"),Q=Symbol.for("react.suspense_list"),ee=Symbol.for("react.memo"),D=Symbol.for("react.lazy"),w=Symbol.for("react.activity"),O=Symbol.for("react.client.reference"),S=k.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,P=Object.prototype.hasOwnProperty,z=Array.isArray,N=console.createTask?console.createTask:function(){return null};k={react_stack_bottom_frame:function(e){return e()}};var ie,ue={},de=k.react_stack_bottom_frame.bind(k,t)(),fe=N(l(t)),_e={};I.Fragment=C,I.jsx=function(e,d,p){var h=1e4>S.recentlyCreatedOwnerStacks++;return m(e,d,p,!1,h?Error("react-stack-top-frame"):de,h?N(l(e)):fe)},I.jsxs=function(e,d,p){var h=1e4>S.recentlyCreatedOwnerStacks++;return m(e,d,p,!0,h?Error("react-stack-top-frame"):de,h?N(l(e)):fe)}})()),I}var se;function ve(){return se||(se=1,process.env.NODE_ENV==="production"?Y.exports=be():Y.exports=he()),Y.exports}var f=ve();const y={overlay:"_overlay_1pta6_15",open:"_open_1pta6_32",center:"_center_1pta6_38",top:"_top_1pta6_44",bottom:"_bottom_1pta6_50",left:"_left_1pta6_55",right:"_right_1pta6_61","top-left":"_top-left_1pta6_66","top-right":"_top-right_1pta6_73","bottom-left":"_bottom-left_1pta6_80","bottom-right":"_bottom-right_1pta6_87",modal:"_modal_1pta6_94",openScale:"_openScale_1pta6_1",sm:"_sm_1pta6_124",md:"_md_1pta6_128",lg:"_lg_1pta6_132",xl:"_xl_1pta6_136",full:"_full_1pta6_140",auto:"_auto_1pta6_147","slide-up":"_slide-up_1pta6_164","slide-down":"_slide-down_1pta6_172",fade:"_fade_1pta6_180","slide-left":"_slide-left_1pta6_188","slide-right":"_slide-right_1pta6_196","drawer-slide-right":"_drawer-slide-right_1pta6_205","drawer-slide-left":"_drawer-slide-left_1pta6_213","drawer-slide-up":"_drawer-slide-up_1pta6_221","drawer-slide-down":"_drawer-slide-down_1pta6_229",header:"_header_1pta6_237",draggable:"_draggable_1pta6_247",title:"_title_1pta6_252",closeButton:"_closeButton_1pta6_259",content:"_content_1pta6_294",footer:"_footer_1pta6_305"};var J={exports:{}};/*!
|
|
23
|
+
Copyright (c) 2018 Jed Watson.
|
|
24
|
+
Licensed under the MIT License (MIT), see
|
|
25
|
+
http://jedwatson.github.io/classnames
|
|
26
|
+
*/var ae;function ge(){return ae||(ae=1,(function(r){(function(){var a={}.hasOwnProperty;function s(){for(var t="",o=0;o<arguments.length;o++){var c=arguments[o];c&&(t=n(t,l(c)))}return t}function l(t){if(typeof t=="string"||typeof t=="number")return t;if(typeof t!="object")return"";if(Array.isArray(t))return s.apply(null,t);if(t.toString!==Object.prototype.toString&&!t.toString.toString().includes("[native code]"))return t.toString();var o="";for(var c in t)a.call(t,c)&&t[c]&&(o=n(o,c));return o}function n(t,o){return o?t?t+" "+o:t+o:t}r.exports?(s.default=s,r.exports=s):window.classNames=s})()})(J)),J.exports}var we=ge();const T=pe(we),q=(r,a)=>{const s=u.useRef(!1),l=u.useRef({x:0,y:0}),n=u.useRef({x:0,y:0}),t=u.useCallback(i=>{if(!s.current||!a.current)return;const _=i.clientX-l.current.x,m=i.clientY-l.current.y,g=n.current.x+_,E=n.current.y+m;a.current.style.transform=`translate(${g}px, ${E}px)`},[a]),o=u.useCallback(i=>{if(!s.current)return;s.current=!1;const _=i.clientX-l.current.x,m=i.clientY-l.current.y;n.current={x:n.current.x+_,y:n.current.y+m},a.current&&(a.current.style.transition=""),document.body.style.userSelect="",window.removeEventListener("mousemove",t),window.removeEventListener("mouseup",o)},[a,t]),c=i=>{!r||!a.current||(s.current=!0,l.current={x:i.clientX,y:i.clientY},a.current.style.transition="none",document.body.style.userSelect="none",window.addEventListener("mousemove",t),window.addEventListener("mouseup",o))};return u.useEffect(()=>()=>{window.removeEventListener("mousemove",t),window.removeEventListener("mouseup",o)},[t,o]),u.useEffect(()=>{r||(n.current={x:0,y:0})},[r]),{onMouseDown:c}},V=({isOpen:r,onClose:a,children:s,title:l,footer:n,closeOnOverlayClick:t=!0,closeOnEsc:o=!0,overlayClassName:c,className:i,overlayStyle:_,style:m,position:g="center",size:E="md",animation:k="zoom",initialFocusRef:$,zIndex:Z,draggable:C=!1,hideHeader:K=!1})=>{const[B,F]=u.useState(!1),[W,U]=u.useState(!1),L=u.useRef(null),{onMouseDown:Q}=q(C,L);if(u.useEffect(()=>{if(r)F(!0),requestAnimationFrame(()=>U(!0));else{U(!1);const w=setTimeout(()=>{F(!1)},200);return()=>clearTimeout(w)}},[r]),u.useEffect(()=>{if(!r||!o)return;const w=O=>{O.key==="Escape"&&a()};return document.addEventListener("keydown",w),()=>document.removeEventListener("keydown",w)},[r,o,a]),u.useEffect(()=>{if(!r||!B)return;const w=L.current;if(!w)return;$!=null&&$.current?$.current.focus():w.focus();const O=w.querySelectorAll('a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'),S=O[0],P=O[O.length-1],z=N=>{N.key==="Tab"&&(N.shiftKey?document.activeElement===S&&(N.preventDefault(),P==null||P.focus()):document.activeElement===P&&(N.preventDefault(),S==null||S.focus()))};return w.addEventListener("keydown",z),()=>w.removeEventListener("keydown",z)},[r,B,$]),u.useEffect(()=>{if(r){const w=window.getComputedStyle(document.body).overflow;return document.body.style.overflow="hidden",()=>{document.body.style.overflow=w}}},[r]),!B)return null;const ee=w=>{w.target===w.currentTarget&&t&&a()},D=f.jsx("div",{className:T(y.overlay,{[y.open]:W},y[g],c),style:{zIndex:Z,..._},onClick:ee,role:"presentation",children:f.jsxs("div",{ref:L,className:T(y.modal,{[y.open]:W},y[E],y[k],i),style:m,role:"dialog","aria-modal":"true","aria-labelledby":l?"modal-title":void 0,tabIndex:-1,children:[!K&&f.jsxs("div",{className:T(y.header,{[y.draggable]:C}),onMouseDown:Q,style:{cursor:C?"move":"default"},children:[l?f.jsx("h2",{id:"modal-title",className:y.title,children:l}):f.jsx("div",{}),f.jsx("button",{type:"button",className:y.closeButton,onClick:a,"aria-label":"Close modal",children:f.jsxs("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2.5",strokeLinecap:"round",strokeLinejoin:"round",children:[f.jsx("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),f.jsx("line",{x1:"6",y1:"6",x2:"18",y2:"18"})]})})]}),f.jsx("div",{className:y.content,children:s}),n&&f.jsx("div",{className:y.footer,children:n})]})});return me.createPortal(D,document.body)},x={drawer:"_drawer_43ihb_24",left:"_left_43ihb_29",right:"_right_43ihb_30",top:"_top_43ihb_35",bottom:"_bottom_43ihb_36",alertModal:"_alertModal_43ihb_41",alertContent:"_alertContent_43ihb_47",icon:"_icon_43ihb_53",success:"_success_43ihb_64",error:"_error_43ihb_69",warning:"_warning_43ihb_74",info:"_info_43ihb_79",okButton:"_okButton_43ihb_84",confirmButton:"_confirmButton_43ihb_85",cancelButton:"_cancelButton_43ihb_107",destructive:"_destructive_43ihb_126"},Ee=({type:r="info",message:a,onOk:s,okText:l="OK",onClose:n,className:t,showHeader:o=!0,...c})=>{const i=()=>{s==null||s(),n()};return f.jsx(V,{...c,onClose:n,className:T(x.alertModal,x[r],t),size:"sm",hideHeader:!o,footer:f.jsx("button",{className:x.okButton,onClick:i,children:l}),children:f.jsxs("div",{className:x.alertContent,children:[f.jsxs("div",{className:x.icon,children:[r==="success"&&"✓",r==="error"&&"✕",r==="warning"&&"!",r==="info"&&"i"]}),f.jsx("div",{className:x.message,children:a})]})})},ye=({message:r,onConfirm:a,onCancel:s,confirmText:l="Confirm",cancelText:n="Cancel",isDestructive:t=!1,onClose:o,className:c,...i})=>{const[_,m]=u.useState(!1),g=async()=>{try{m(!0),await a(),o()}catch(k){console.error(k)}finally{m(!1)}},E=()=>{s==null||s(),o()};return f.jsx(V,{...i,onClose:o,className:T(x.confirmModal,c),size:"sm",closeOnOverlayClick:!_,closeOnEsc:!_,footer:f.jsxs(f.Fragment,{children:[f.jsx("button",{className:x.cancelButton,onClick:E,disabled:_,children:n}),f.jsx("button",{className:T(x.confirmButton,{[x.destructive]:t}),onClick:g,disabled:_,children:_?"Processing...":l})]}),children:f.jsx("div",{className:x.confirmContent,children:r})})},xe=({placement:r="right",className:a,...s})=>{let l="right",n="slide-right";switch(r){case"left":l="left",n="drawer-slide-left";break;case"top":l="top",n="drawer-slide-down";break;case"bottom":l="bottom",n="drawer-slide-up";break;case"right":default:l="right",n="drawer-slide-right";break}return f.jsx(V,{...s,position:l,animation:n,className:T(x.drawer,x[r],a)})},R={wrapper:"_wrapper_1bfkp_1",trigger:"_trigger_1bfkp_6",popover:"_popover_1bfkp_11",fadeIn:"_fadeIn_1bfkp_1",draggable:"_draggable_1bfkp_24",bottom:"_bottom_1bfkp_30",top:"_top_1bfkp_37",left:"_left_1bfkp_44",right:"_right_1bfkp_51"},le=(r,a)=>{u.useEffect(()=>{const s=l=>{!r.current||r.current.contains(l.target)||a(l)};return document.addEventListener("mousedown",s),document.addEventListener("touchstart",s),()=>{document.removeEventListener("mousedown",s),document.removeEventListener("touchstart",s)}},[r,a])},Te=({children:r,content:a,position:s="bottom",className:l,style:n,width:t,draggable:o=!1})=>{const[c,i]=u.useState(!1),_=u.useRef(null),m=u.useRef(null);le(_,()=>{c&&i(!1)});const{onMouseDown:g}=q(o,m),E=()=>i(!c);return f.jsxs("div",{className:R.wrapper,ref:_,children:[f.jsx("div",{onClick:E,className:R.trigger,children:r}),c&&f.jsx("div",{ref:m,className:T(R.popover,R[s],l,{[R.draggable]:o}),style:{width:t,...n},role:"dialog","aria-modal":"false",onMouseDown:g,children:typeof a=="function"?a(()=>i(!1)):a})]})},j={toast:"_toast_15h14_1",slideIn:"_slideIn_15h14_1",draggable:"_draggable_15h14_19",exiting:"_exiting_15h14_24",success:"_success_15h14_31",error:"_error_15h14_35",warning:"_warning_15h14_39",info:"_info_15h14_43",icon:"_icon_15h14_47",content:"_content_15h14_78",closeBtn:"_closeBtn_15h14_85",container:"_container_15h14_119","top-left":"_top-left_15h14_129","top-right":"_top-right_15h14_135","top-center":"_top-center_15h14_141","bottom-left":"_bottom-left_15h14_148","bottom-right":"_bottom-right_15h14_155","bottom-center":"_bottom-center_15h14_162",slideInBottom:"_slideInBottom_15h14_1"},ce=({id:r,type:a,content:s,duration:l=3e3,onDismiss:n,draggable:t=!1})=>{const[o,c]=u.useState(!1),i=u.useRef(null),{onMouseDown:_}=q(t,i),m=u.useCallback(()=>{c(!0),setTimeout(()=>{n(r)},300)},[r,n]);return u.useEffect(()=>{if(l>0){const g=setTimeout(()=>{m()},l);return()=>clearTimeout(g)}},[l,m]),f.jsxs("div",{ref:i,className:T(j.toast,j[a],{[j.exiting]:o,[j.draggable]:t}),role:"alert",onMouseDown:_,children:[f.jsxs("div",{className:j.icon,children:[a==="success"&&"✓",a==="error"&&"✕",a==="warning"&&"!",a==="info"&&"i"]}),f.jsx("div",{className:j.content,children:s}),f.jsx("button",{className:j.closeBtn,onClick:m,children:"×"})]})},G=u.createContext(void 0);let ke=0;const je=({children:r})=>{const[a,s]=u.useState([]),l=u.useCallback(({closeOthers:c,delay:i=0,..._})=>{const m=`toast-${ke++}`,g={..._,duration:_.duration,delay:i,id:m},E=()=>{s(k=>c?[g]:[...k,g])};i>0?setTimeout(E,i):E()},[]),n=u.useCallback(c=>{s(i=>i.filter(_=>_.id!==c))},[]),t=u.useCallback(()=>{s([])},[]),o=a.reduce((c,i)=>{const _=i.position||"top-right";return c[_]||(c[_]=[]),c[_].push(i),c},{});return f.jsxs(G.Provider,{value:{addToast:l,removeToast:n,clearAll:t},children:[r,Object.keys(o).map(c=>f.jsx("div",{className:T(j.container,j[c]),children:o[c].map(i=>f.jsx(ce,{...i,onDismiss:n},i.id))},c))]})},$e=()=>{const r=u.useContext(G);if(!r)throw new Error("useToast must be used within a ToastProvider");const{addToast:a,removeToast:s,clearAll:l}=r,n=(t,o,c={})=>{let i,_={...c};if(typeof o=="object"&&o!==null&&"content"in o&&!u.isValidElement(o)){const m=o;i=m.content,_={..._,...m}}else i=o;a({type:t,content:i,duration:3e3,position:"top-right",..._})};return{success:(t,o)=>n("success",t,o),error:(t,o)=>n("error",t,o),warning:(t,o)=>n("warning",t,o),info:(t,o)=>n("info",t,o),dismiss:t=>s(t),dismissAll:()=>l()}},H={wrapper:"_wrapper_1dham_1",tooltip:"_tooltip_1dham_6",fadeIn:"_fadeIn_1dham_1",top:"_top_1dham_22",bottom:"_bottom_1dham_28",left:"_left_1dham_34",right:"_right_1dham_40"},Se=({children:r,content:a,position:s="top",delay:l=200,width:n,height:t,className:o,style:c})=>{const[i,_]=u.useState(!1),m=u.useRef(null),g=()=>{m.current=setTimeout(()=>{_(!0)},l)},E=()=>{m.current&&clearTimeout(m.current),_(!1)};return f.jsxs("div",{className:H.wrapper,onMouseEnter:g,onMouseLeave:E,onFocus:g,onBlur:E,children:[r,i&&f.jsx("div",{className:T(H.tooltip,H[s],o),style:{width:n,height:t,...c},role:"tooltip",children:a})]})},Ne=(r=!1)=>{const[a,s]=u.useState(r),l=u.useCallback(()=>s(!0),[]),n=u.useCallback(()=>s(!1),[]),t=u.useCallback(()=>s(o=>!o),[]);return{isOpen:a,open:l,close:n,toggle:t}};b.AlertModal=Ee,b.ConfirmModal=ye,b.Drawer=xe,b.Modal=V,b.Popover=Te,b.Toast=ce,b.ToastContext=G,b.ToastProvider=je,b.Tooltip=Se,b.useClickOutside=le,b.useDraggable=q,b.useModal=Ne,b.useToast=$e,Object.defineProperty(b,Symbol.toStringTag,{value:"Module"})}));
|
package/package.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@myzbox/react-overlay",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A comprehensive React overlay library with modals, drawers, tooltips, popovers, and toast notifications",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"modal",
|
|
8
|
+
"dialog",
|
|
9
|
+
"overlay",
|
|
10
|
+
"drawer",
|
|
11
|
+
"tooltip",
|
|
12
|
+
"popover",
|
|
13
|
+
"toast",
|
|
14
|
+
"notification",
|
|
15
|
+
"alert",
|
|
16
|
+
"confirm",
|
|
17
|
+
"draggable",
|
|
18
|
+
"accessible",
|
|
19
|
+
"typescript",
|
|
20
|
+
"react-component",
|
|
21
|
+
"ui",
|
|
22
|
+
"myzbox"
|
|
23
|
+
],
|
|
24
|
+
"author": "myzbox",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/myzbox/react-overlay"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/myzbox/react-overlay/issues"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/myzbox/react-overlay#readme",
|
|
34
|
+
"type": "module",
|
|
35
|
+
"main": "./dist/react-overlay.umd.js",
|
|
36
|
+
"module": "./dist/react-overlay.es.js",
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"exports": {
|
|
39
|
+
".": {
|
|
40
|
+
"types": "./dist/index.d.ts",
|
|
41
|
+
"import": "./dist/react-overlay.es.js",
|
|
42
|
+
"require": "./dist/react-overlay.umd.js"
|
|
43
|
+
},
|
|
44
|
+
"./dist/react-overlay.css": "./dist/react-overlay.css",
|
|
45
|
+
"./dist/style.css": "./dist/react-overlay.css",
|
|
46
|
+
"./types/*": "./src/types/*",
|
|
47
|
+
"./styles/*": "./src/styles/*",
|
|
48
|
+
"./hooks/*": "./src/hooks/*"
|
|
49
|
+
},
|
|
50
|
+
"files": [
|
|
51
|
+
"dist",
|
|
52
|
+
"src"
|
|
53
|
+
],
|
|
54
|
+
"scripts": {
|
|
55
|
+
"dev": "vite",
|
|
56
|
+
"build": "tsc -b && vite build",
|
|
57
|
+
"lint": "eslint .",
|
|
58
|
+
"preview": "vite preview"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"react": "^19.0.0",
|
|
62
|
+
"react-dom": "^19.0.0"
|
|
63
|
+
},
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"classnames": "^2.5.1"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@eslint/js": "^9.17.0",
|
|
69
|
+
"@testing-library/user-event": "^14.6.1",
|
|
70
|
+
"@types/node": "^22.10.2",
|
|
71
|
+
"@types/react": "^19.0.2",
|
|
72
|
+
"@types/react-dom": "^19.0.2",
|
|
73
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
74
|
+
"eslint": "^9.17.0",
|
|
75
|
+
"eslint-plugin-react-hooks": "^5.0.0",
|
|
76
|
+
"eslint-plugin-react-refresh": "^0.4.16",
|
|
77
|
+
"globals": "^15.14.0",
|
|
78
|
+
"react": "^19.0.0",
|
|
79
|
+
"react-dom": "^19.0.0",
|
|
80
|
+
"typescript": "~5.6.2",
|
|
81
|
+
"typescript-eslint": "^8.18.1",
|
|
82
|
+
"vite": "^6.0.5",
|
|
83
|
+
"vite-plugin-dts": "^4.4.0"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './modal/Modal';
|
|
2
|
+
export type { ModalProps, ModalSize, ModalPosition, ModalAnimation } from '../types/Modal';
|
|
3
|
+
export * from './modal-variants/AlertModal';
|
|
4
|
+
export * from './modal-variants/ConfirmModal';
|
|
5
|
+
export * from './modal-variants/Drawer';
|
|
6
|
+
export * from './popover/Popover';
|
|
7
|
+
export type { PopoverProps } from '../types/Popover';
|
|
8
|
+
export * from './toast';
|
|
9
|
+
export type { ToastPosition, ToastOptions } from '../types/Toast';
|
|
10
|
+
export * from './tooltip/Tooltip';
|
|
11
|
+
export type { TooltipProps } from '../types/Tooltip';
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import styles from '../../styles/Modal.module.css';
|
|
4
|
+
import type { ModalProps } from '../../types/Modal';
|
|
5
|
+
import classNames from 'classnames';
|
|
6
|
+
import { useDraggable } from '../../hooks/useDraggable';
|
|
7
|
+
|
|
8
|
+
export const Modal: React.FC<ModalProps> = ({
|
|
9
|
+
isOpen,
|
|
10
|
+
onClose,
|
|
11
|
+
children,
|
|
12
|
+
title,
|
|
13
|
+
footer,
|
|
14
|
+
closeOnOverlayClick = true,
|
|
15
|
+
closeOnEsc = true,
|
|
16
|
+
overlayClassName,
|
|
17
|
+
className,
|
|
18
|
+
overlayStyle,
|
|
19
|
+
style,
|
|
20
|
+
position = 'center',
|
|
21
|
+
size = 'md',
|
|
22
|
+
animation = 'zoom',
|
|
23
|
+
initialFocusRef,
|
|
24
|
+
|
|
25
|
+
zIndex,
|
|
26
|
+
draggable = false,
|
|
27
|
+
hideHeader = false,
|
|
28
|
+
}) => {
|
|
29
|
+
const [mounted, setMounted] = useState(false);
|
|
30
|
+
const [visible, setVisible] = useState(false);
|
|
31
|
+
const modalRef = useRef<HTMLDivElement>(null);
|
|
32
|
+
const { onMouseDown } = useDraggable(draggable, modalRef as React.RefObject<HTMLElement>);
|
|
33
|
+
|
|
34
|
+
// Handle mounting for transition
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (isOpen) {
|
|
37
|
+
setMounted(true);
|
|
38
|
+
// Small delay to allow CSS transition to initiate
|
|
39
|
+
requestAnimationFrame(() => setVisible(true));
|
|
40
|
+
} else {
|
|
41
|
+
setVisible(false);
|
|
42
|
+
const timer = setTimeout(() => {
|
|
43
|
+
setMounted(false);
|
|
44
|
+
}, 200); // Match CSS transition duration
|
|
45
|
+
return () => clearTimeout(timer);
|
|
46
|
+
}
|
|
47
|
+
}, [isOpen]);
|
|
48
|
+
|
|
49
|
+
// Handle ESC key
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!isOpen || !closeOnEsc) return;
|
|
52
|
+
|
|
53
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
54
|
+
if (event.key === 'Escape') {
|
|
55
|
+
onClose();
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
60
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
61
|
+
}, [isOpen, closeOnEsc, onClose]);
|
|
62
|
+
|
|
63
|
+
// Handle Focus Trap (Senior level implementation: simple but effective manual trap for minimal deps)
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!isOpen || !mounted) return;
|
|
66
|
+
|
|
67
|
+
const modalElement = modalRef.current;
|
|
68
|
+
if (!modalElement) return;
|
|
69
|
+
|
|
70
|
+
// Focus initial element or modal itself
|
|
71
|
+
if (initialFocusRef?.current) {
|
|
72
|
+
initialFocusRef.current.focus();
|
|
73
|
+
} else {
|
|
74
|
+
modalElement.focus();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const focusableElements = modalElement.querySelectorAll(
|
|
78
|
+
'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
79
|
+
);
|
|
80
|
+
const firstElement = focusableElements[0] as HTMLElement;
|
|
81
|
+
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
|
|
82
|
+
|
|
83
|
+
const handleTab = (e: KeyboardEvent) => {
|
|
84
|
+
if (e.key === 'Tab') {
|
|
85
|
+
if (e.shiftKey) {
|
|
86
|
+
if (document.activeElement === firstElement) {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
lastElement?.focus();
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
if (document.activeElement === lastElement) {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
firstElement?.focus();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
modalElement.addEventListener('keydown', handleTab);
|
|
100
|
+
return () => modalElement.removeEventListener('keydown', handleTab);
|
|
101
|
+
}, [isOpen, mounted, initialFocusRef]);
|
|
102
|
+
|
|
103
|
+
// Lock Body Scroll
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (isOpen) {
|
|
106
|
+
const originalStyle = window.getComputedStyle(document.body).overflow;
|
|
107
|
+
document.body.style.overflow = 'hidden';
|
|
108
|
+
return () => {
|
|
109
|
+
document.body.style.overflow = originalStyle;
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}, [isOpen]);
|
|
113
|
+
|
|
114
|
+
if (!mounted) return null;
|
|
115
|
+
|
|
116
|
+
const handleOverlayClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
117
|
+
if (e.target === e.currentTarget && closeOnOverlayClick) {
|
|
118
|
+
onClose();
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const portalContent = (
|
|
123
|
+
<div
|
|
124
|
+
className={classNames(styles.overlay, { [styles.open]: visible }, styles[position], overlayClassName)}
|
|
125
|
+
style={{ zIndex, ...overlayStyle }}
|
|
126
|
+
onClick={handleOverlayClick}
|
|
127
|
+
role="presentation"
|
|
128
|
+
>
|
|
129
|
+
<div
|
|
130
|
+
ref={modalRef}
|
|
131
|
+
className={classNames(
|
|
132
|
+
styles.modal,
|
|
133
|
+
{ [styles.open]: visible },
|
|
134
|
+
styles[size],
|
|
135
|
+
styles[animation],
|
|
136
|
+
className
|
|
137
|
+
)}
|
|
138
|
+
style={style}
|
|
139
|
+
role="dialog"
|
|
140
|
+
aria-modal="true"
|
|
141
|
+
aria-labelledby={title ? 'modal-title' : undefined}
|
|
142
|
+
tabIndex={-1}
|
|
143
|
+
>
|
|
144
|
+
{!hideHeader && (
|
|
145
|
+
<div
|
|
146
|
+
className={classNames(styles.header, { [styles.draggable]: draggable })}
|
|
147
|
+
onMouseDown={onMouseDown}
|
|
148
|
+
style={{ cursor: draggable ? 'move' : 'default' }}
|
|
149
|
+
>
|
|
150
|
+
{title ? (
|
|
151
|
+
<h2 id="modal-title" className={styles.title}>{title}</h2>
|
|
152
|
+
) : (
|
|
153
|
+
<div /> // Placeholder to keep button on right
|
|
154
|
+
)}
|
|
155
|
+
<button
|
|
156
|
+
type="button"
|
|
157
|
+
className={styles.closeButton}
|
|
158
|
+
onClick={onClose}
|
|
159
|
+
aria-label="Close modal"
|
|
160
|
+
>
|
|
161
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
162
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
163
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
164
|
+
</svg>
|
|
165
|
+
</button>
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
|
|
169
|
+
<div className={styles.content}>
|
|
170
|
+
{children}
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
{footer && (
|
|
174
|
+
<div className={styles.footer}>
|
|
175
|
+
{footer}
|
|
176
|
+
</div>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
return createPortal(portalContent, document.body);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export default Modal;
|
|
186
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Modal } from '../modal/Modal';
|
|
3
|
+
import type { ModalProps } from '../../types/Modal';
|
|
4
|
+
import styles from '../../styles/ModalVariants.module.css';
|
|
5
|
+
import classNames from 'classnames';
|
|
6
|
+
|
|
7
|
+
export type AlertType = 'success' | 'error' | 'warning' | 'info';
|
|
8
|
+
|
|
9
|
+
export interface AlertModalProps extends Omit<ModalProps, 'children' | 'footer'> {
|
|
10
|
+
type?: AlertType;
|
|
11
|
+
message: React.ReactNode;
|
|
12
|
+
onOk?: () => void;
|
|
13
|
+
okText?: string;
|
|
14
|
+
showHeader?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const AlertModal: React.FC<AlertModalProps> = ({
|
|
18
|
+
type = 'info',
|
|
19
|
+
message,
|
|
20
|
+
onOk,
|
|
21
|
+
okText = 'OK',
|
|
22
|
+
onClose,
|
|
23
|
+
className,
|
|
24
|
+
showHeader = true,
|
|
25
|
+
...props
|
|
26
|
+
}) => {
|
|
27
|
+
const handleOk = () => {
|
|
28
|
+
onOk?.();
|
|
29
|
+
onClose();
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Modal
|
|
34
|
+
{...props}
|
|
35
|
+
onClose={onClose}
|
|
36
|
+
className={classNames(styles.alertModal, styles[type], className)}
|
|
37
|
+
size="sm"
|
|
38
|
+
hideHeader={!showHeader}
|
|
39
|
+
footer={
|
|
40
|
+
<button className={styles.okButton} onClick={handleOk}>
|
|
41
|
+
{okText}
|
|
42
|
+
</button>
|
|
43
|
+
}
|
|
44
|
+
>
|
|
45
|
+
<div className={styles.alertContent}>
|
|
46
|
+
<div className={styles.icon}>
|
|
47
|
+
{type === 'success' && '✓'}
|
|
48
|
+
{type === 'error' && '✕'}
|
|
49
|
+
{type === 'warning' && '!'}
|
|
50
|
+
{type === 'info' && 'i'}
|
|
51
|
+
</div>
|
|
52
|
+
<div className={styles.message}>{message}</div>
|
|
53
|
+
</div>
|
|
54
|
+
</Modal>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Modal } from '../modal/Modal';
|
|
3
|
+
import type { ModalProps } from '../../types/Modal';
|
|
4
|
+
import styles from '../../styles/ModalVariants.module.css';
|
|
5
|
+
import classNames from 'classnames';
|
|
6
|
+
|
|
7
|
+
export interface ConfirmModalProps extends Omit<ModalProps, 'children' | 'footer'> {
|
|
8
|
+
message: React.ReactNode;
|
|
9
|
+
onConfirm: () => void | Promise<void>;
|
|
10
|
+
onCancel?: () => void;
|
|
11
|
+
confirmText?: string;
|
|
12
|
+
cancelText?: string;
|
|
13
|
+
isDestructive?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const ConfirmModal: React.FC<ConfirmModalProps> = ({
|
|
17
|
+
message,
|
|
18
|
+
onConfirm,
|
|
19
|
+
onCancel,
|
|
20
|
+
confirmText = 'Confirm',
|
|
21
|
+
cancelText = 'Cancel',
|
|
22
|
+
isDestructive = false,
|
|
23
|
+
onClose,
|
|
24
|
+
className,
|
|
25
|
+
...props
|
|
26
|
+
}) => {
|
|
27
|
+
const [loading, setLoading] = useState(false);
|
|
28
|
+
|
|
29
|
+
const handleConfirm = async () => {
|
|
30
|
+
try {
|
|
31
|
+
setLoading(true);
|
|
32
|
+
await onConfirm();
|
|
33
|
+
onClose();
|
|
34
|
+
} catch (e) {
|
|
35
|
+
console.error(e);
|
|
36
|
+
} finally {
|
|
37
|
+
setLoading(false);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const handleCancel = () => {
|
|
42
|
+
onCancel?.();
|
|
43
|
+
onClose();
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Modal
|
|
48
|
+
{...props}
|
|
49
|
+
onClose={onClose}
|
|
50
|
+
className={classNames(styles.confirmModal, className)}
|
|
51
|
+
size="sm"
|
|
52
|
+
closeOnOverlayClick={!loading}
|
|
53
|
+
closeOnEsc={!loading}
|
|
54
|
+
footer={
|
|
55
|
+
<>
|
|
56
|
+
<button
|
|
57
|
+
className={styles.cancelButton}
|
|
58
|
+
onClick={handleCancel}
|
|
59
|
+
disabled={loading}
|
|
60
|
+
>
|
|
61
|
+
{cancelText}
|
|
62
|
+
</button>
|
|
63
|
+
<button
|
|
64
|
+
className={classNames(styles.confirmButton, { [styles.destructive]: isDestructive })}
|
|
65
|
+
onClick={handleConfirm}
|
|
66
|
+
disabled={loading}
|
|
67
|
+
>
|
|
68
|
+
{loading ? 'Processing...' : confirmText}
|
|
69
|
+
</button>
|
|
70
|
+
</>
|
|
71
|
+
}
|
|
72
|
+
>
|
|
73
|
+
<div className={styles.confirmContent}>
|
|
74
|
+
{message}
|
|
75
|
+
</div>
|
|
76
|
+
</Modal>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Modal } from '../modal/Modal';
|
|
3
|
+
import type { ModalProps, ModalPosition } from '../../types/Modal';
|
|
4
|
+
import classNames from 'classnames';
|
|
5
|
+
import styles from '../../styles/ModalVariants.module.css'; // We might want shared drawer styles here or in Modal.module.css
|
|
6
|
+
|
|
7
|
+
export interface DrawerProps extends ModalProps {
|
|
8
|
+
placement?: 'left' | 'right' | 'top' | 'bottom';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const Drawer: React.FC<DrawerProps> = ({
|
|
12
|
+
placement = 'right',
|
|
13
|
+
className,
|
|
14
|
+
...props
|
|
15
|
+
}) => {
|
|
16
|
+
|
|
17
|
+
// Map placement to Modal position and animation
|
|
18
|
+
let position: ModalPosition = 'right';
|
|
19
|
+
let animation: ModalProps['animation'] = 'slide-right';
|
|
20
|
+
|
|
21
|
+
switch (placement) {
|
|
22
|
+
case 'left':
|
|
23
|
+
position = 'left';
|
|
24
|
+
animation = 'drawer-slide-left' as ModalProps['animation'];
|
|
25
|
+
break;
|
|
26
|
+
case 'top':
|
|
27
|
+
position = 'top';
|
|
28
|
+
animation = 'drawer-slide-down' as ModalProps['animation'];
|
|
29
|
+
break;
|
|
30
|
+
case 'bottom':
|
|
31
|
+
position = 'bottom';
|
|
32
|
+
animation = 'drawer-slide-up' as ModalProps['animation'];
|
|
33
|
+
break;
|
|
34
|
+
case 'right':
|
|
35
|
+
default:
|
|
36
|
+
position = 'right';
|
|
37
|
+
animation = 'drawer-slide-right' as ModalProps['animation'];
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Modal
|
|
43
|
+
{...props}
|
|
44
|
+
position={position}
|
|
45
|
+
animation={animation}
|
|
46
|
+
className={classNames(styles.drawer, styles[placement], className)}
|
|
47
|
+
// Drawers usually are full height (for left/right) or full width (for top/bottom)
|
|
48
|
+
// But we can let styles handle that based on position
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React, { useState, useRef } from 'react';
|
|
2
|
+
import type { PopoverProps } from '../../types/Popover';
|
|
3
|
+
import styles from '../../styles/Popover.module.css';
|
|
4
|
+
import classNames from 'classnames';
|
|
5
|
+
import { useClickOutside } from '../../hooks/useClickOutside';
|
|
6
|
+
import { useDraggable } from '../../hooks/useDraggable';
|
|
7
|
+
|
|
8
|
+
export const Popover: React.FC<PopoverProps> = ({
|
|
9
|
+
children,
|
|
10
|
+
content,
|
|
11
|
+
position = 'bottom',
|
|
12
|
+
className,
|
|
13
|
+
style,
|
|
14
|
+
width,
|
|
15
|
+
draggable = false,
|
|
16
|
+
}) => {
|
|
17
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
18
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
19
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
20
|
+
|
|
21
|
+
useClickOutside(containerRef as React.RefObject<HTMLElement>, () => {
|
|
22
|
+
if (isOpen) setIsOpen(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const { onMouseDown } = useDraggable(draggable, contentRef as React.RefObject<HTMLElement>);
|
|
26
|
+
|
|
27
|
+
const toggleOpen = () => setIsOpen(!isOpen);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className={styles.wrapper} ref={containerRef}>
|
|
31
|
+
<div onClick={toggleOpen} className={styles.trigger}>
|
|
32
|
+
{children}
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
{isOpen && (
|
|
36
|
+
<div
|
|
37
|
+
ref={contentRef}
|
|
38
|
+
className={classNames(
|
|
39
|
+
styles.popover,
|
|
40
|
+
styles[position],
|
|
41
|
+
className,
|
|
42
|
+
{ [styles.draggable]: draggable }
|
|
43
|
+
)}
|
|
44
|
+
style={{ width, ...style }}
|
|
45
|
+
role="dialog"
|
|
46
|
+
aria-modal="false"
|
|
47
|
+
onMouseDown={onMouseDown}
|
|
48
|
+
>
|
|
49
|
+
{typeof content === 'function' ? content(() => setIsOpen(false)) : content}
|
|
50
|
+
</div>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import type { ToastProps } from '../../types/Toast';
|
|
3
|
+
import styles from '../../styles/Toast.module.css';
|
|
4
|
+
import classNames from 'classnames';
|
|
5
|
+
import { useDraggable } from '../../hooks/useDraggable';
|
|
6
|
+
import { useRef } from 'react';
|
|
7
|
+
|
|
8
|
+
export const Toast: React.FC<ToastProps> = ({
|
|
9
|
+
id,
|
|
10
|
+
type,
|
|
11
|
+
content,
|
|
12
|
+
duration = 3000,
|
|
13
|
+
onDismiss,
|
|
14
|
+
draggable = false,
|
|
15
|
+
}) => {
|
|
16
|
+
const [isExiting, setIsExiting] = useState(false);
|
|
17
|
+
const toastRef = useRef<HTMLDivElement>(null);
|
|
18
|
+
const { onMouseDown } = useDraggable(draggable, toastRef as React.RefObject<HTMLElement>);
|
|
19
|
+
|
|
20
|
+
const handleDismiss = React.useCallback(() => {
|
|
21
|
+
setIsExiting(true);
|
|
22
|
+
// Wait for animation to finish before removing from DOM
|
|
23
|
+
setTimeout(() => {
|
|
24
|
+
onDismiss(id);
|
|
25
|
+
}, 300); // 300ms matches CSS animation
|
|
26
|
+
}, [id, onDismiss]);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (duration > 0) {
|
|
30
|
+
const timer = setTimeout(() => {
|
|
31
|
+
handleDismiss();
|
|
32
|
+
}, duration);
|
|
33
|
+
return () => clearTimeout(timer);
|
|
34
|
+
}
|
|
35
|
+
}, [duration, handleDismiss]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
ref={toastRef}
|
|
40
|
+
className={classNames(
|
|
41
|
+
styles.toast,
|
|
42
|
+
styles[type],
|
|
43
|
+
{
|
|
44
|
+
[styles.exiting]: isExiting,
|
|
45
|
+
[styles.draggable]: draggable
|
|
46
|
+
}
|
|
47
|
+
)}
|
|
48
|
+
role="alert"
|
|
49
|
+
onMouseDown={onMouseDown}
|
|
50
|
+
>
|
|
51
|
+
<div className={styles.icon}>
|
|
52
|
+
{type === 'success' && '✓'}
|
|
53
|
+
{type === 'error' && '✕'}
|
|
54
|
+
{type === 'warning' && '!'}
|
|
55
|
+
{type === 'info' && 'i'}
|
|
56
|
+
</div>
|
|
57
|
+
<div className={styles.content}>{content}</div>
|
|
58
|
+
<button className={styles.closeBtn} onClick={handleDismiss}>×</button>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
};
|