react-imperial-modal 1.0.5 → 2.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/README.md CHANGED
@@ -92,16 +92,6 @@ const Prompt = function(props) {
92
92
  <button onClick={() => close(promptValue)}>ok</button>
93
93
  </div>
94
94
  }
95
-
96
- const Alert = function(props) {
97
- const { message, close } = props
98
-
99
- return <div>
100
- <h1>Alert</h1>
101
- <p>{message}</p>
102
- <button onClick={close}>ok</button>
103
- </div>
104
- }
105
95
  ```
106
96
 
107
97
  you can:
@@ -0,0 +1,7 @@
1
+ import { ModalEntry } from './types';
2
+ type InternalModalProps<T, P> = {
3
+ entry: ModalEntry<T, P>;
4
+ className?: string;
5
+ };
6
+ declare const Modal: <T, P>({ className, entry }: InternalModalProps<T, P>) => import("react/jsx-runtime").JSX.Element;
7
+ export default Modal;
@@ -0,0 +1,4 @@
1
+ import { default as React } from 'react';
2
+ import { ModalContextValue, ModalProviderProps } from './types';
3
+ export declare const ModalContext: React.Context<ModalContextValue>;
4
+ export declare const ModalProvider: ({ children, config }: ModalProviderProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,7 @@
1
+ export declare const withResolvers: <T>() => {
2
+ promise: Promise<T>;
3
+ resolve: (value: T | PromiseLike<T>) => void;
4
+ reject: (reason?: unknown) => void;
5
+ };
6
+ export declare const ESC_KEY = "Escape";
7
+ export declare const focusableSelector: string;
@@ -0,0 +1,5 @@
1
+ import { ModalProvider } from './ModalProvider';
2
+ import { useModal } from './useModal';
3
+ import { ModalProps, ModalProviderConfig } from './types';
4
+ export { ModalProvider, useModal };
5
+ export type { ModalProps, ModalProviderConfig };
@@ -0,0 +1,2 @@
1
+ export * from './index'
2
+ export {}
@@ -0,0 +1,142 @@
1
+ import { jsx as C, jsxs as O } from "react/jsx-runtime";
2
+ import I, { useRef as P, useContext as A, useLayoutEffect as K, useCallback as u, useState as L, useMemo as R } from "react";
3
+ const q = () => {
4
+ let s, t;
5
+ return { promise: new Promise((i, l) => {
6
+ s = i, t = l;
7
+ }), resolve: s, reject: t };
8
+ }, D = "Escape", N = [
9
+ "a[href]:not([tabindex='-1'])",
10
+ "area[href]:not([tabindex='-1'])",
11
+ "input:not([disabled]):not([tabindex='-1'])",
12
+ "select:not([disabled]):not([tabindex='-1'])",
13
+ "textarea:not([disabled]):not([tabindex='-1'])",
14
+ "button:not([disabled]):not([tabindex='-1'])",
15
+ "iframe:not([tabindex='-1'])",
16
+ "[tabindex]:not([tabindex='-1'])",
17
+ "[contentEditable=true]:not([tabindex='-1'])"
18
+ ].join(", "), B = function({ className: s, entry: t }) {
19
+ const { role: a = "dialog", label: i, labelledby: l, componentProps: d, Component: M } = t, n = P(null), { removeModal: m } = A(S);
20
+ K(() => {
21
+ requestAnimationFrame(() => {
22
+ var r;
23
+ n.current && (n.current.showModal(), (r = n.current.querySelector(N)) == null || r.focus());
24
+ });
25
+ }, []);
26
+ const f = u(
27
+ (r) => {
28
+ r.key === D && !t.ignoreEscape && m(t.instanceId);
29
+ },
30
+ [t, m]
31
+ );
32
+ return /* @__PURE__ */ C(
33
+ "dialog",
34
+ {
35
+ ref: n,
36
+ role: a,
37
+ "aria-label": i,
38
+ "aria-labelledby": l,
39
+ className: s,
40
+ onKeyDown: f,
41
+ children: /* @__PURE__ */ C(
42
+ M,
43
+ {
44
+ close: t.closeModal,
45
+ resolve: t.resolveModal,
46
+ reject: t.rejectModal,
47
+ ...d
48
+ }
49
+ )
50
+ }
51
+ );
52
+ }, g = () => {
53
+ throw new Error(
54
+ "Attempted to call useModal outside of modal context. Make sure your component is inside ModalProvider."
55
+ );
56
+ }, S = I.createContext({
57
+ addModal: g,
58
+ removeModal: g,
59
+ resolveModal: g,
60
+ rejectModal: g
61
+ }), F = {
62
+ bodyOpenClass: "modal-open",
63
+ modalContainerClass: "modals",
64
+ modalClass: "modal"
65
+ }, w = document.documentElement, k = document.body, Y = ({ children: s, config: t = {} }) => {
66
+ const [a, i] = L([]), l = R(() => ({ ...F, ...t }), [t]), d = P(null), M = P([]), n = a.at(-1), m = u(() => {
67
+ var e;
68
+ k.classList.add(l.bodyOpenClass), (e = d == null ? void 0 : d.current) == null || e.setAttribute("aria-hidden", "true"), w.style.overflow = "hidden";
69
+ }, [l]), f = u(() => {
70
+ var e;
71
+ k.classList.remove(l.bodyOpenClass), (e = d == null ? void 0 : d.current) == null || e.removeAttribute("aria-hidden"), w.style.overflow = "";
72
+ }, [l]), r = u(
73
+ (e) => {
74
+ i((o) => o.includes(e) ? o : (o.length === 0 && m(), M.current.push(document.activeElement), [...o, e]));
75
+ },
76
+ [m]
77
+ ), p = u(
78
+ (e) => {
79
+ i((o) => {
80
+ const E = o.find((y) => y.instanceId === e), c = e === void 0 ? n : E, v = o.length === 1, j = M.current.pop();
81
+ return !c || !o.includes(c) ? o : (v && j && w.contains(j) && j.focus(), v && f(), o.filter((y) => c !== y));
82
+ });
83
+ },
84
+ [f, n]
85
+ ), b = u(
86
+ (e, o) => {
87
+ const E = a.find((v) => v.instanceId === o), c = o === void 0 ? n : E;
88
+ c == null || c.resolveModal(e || null);
89
+ },
90
+ [a, n]
91
+ ), x = u(
92
+ (e, o) => {
93
+ const E = a.find((v) => v.instanceId === o), c = o === void 0 ? n : E;
94
+ c == null || c.rejectModal(e || null);
95
+ },
96
+ [a, n]
97
+ ), h = R(
98
+ () => ({ addModal: r, removeModal: p, resolveModal: b, rejectModal: x }),
99
+ [r, p, b, x]
100
+ );
101
+ return /* @__PURE__ */ O(S.Provider, { value: h, children: [
102
+ /* @__PURE__ */ C("div", { ref: d, children: s }),
103
+ /* @__PURE__ */ C("div", { className: l.modalContainerClass, children: a.map((e) => /* @__PURE__ */ C(
104
+ B,
105
+ {
106
+ className: l.modalClass,
107
+ entry: e
108
+ },
109
+ e.instanceId
110
+ )) })
111
+ ] });
112
+ };
113
+ let T = 1;
114
+ const _ = () => {
115
+ const s = A(S);
116
+ return [u(
117
+ (a, i, l = !1, d, M, n = "dialog") => {
118
+ const { promise: m, resolve: f, reject: r } = q(), p = (T++ / 5).toString(32), b = () => s.removeModal(p), x = Object.assign(m, {
119
+ instanceId: p,
120
+ Component: a,
121
+ componentProps: i,
122
+ ignoreEscape: l,
123
+ labelledby: M,
124
+ label: d,
125
+ role: n,
126
+ resolveModal: (h) => {
127
+ f(h), b();
128
+ },
129
+ rejectModal: (h) => {
130
+ r(h), b();
131
+ },
132
+ closeModal: b
133
+ });
134
+ return s.addModal(x), x;
135
+ },
136
+ [s]
137
+ ), s.resolveModal, s.rejectModal];
138
+ };
139
+ export {
140
+ Y as ModalProvider,
141
+ _ as useModal
142
+ };
@@ -0,0 +1 @@
1
+ (function(u,c){typeof exports=="object"&&typeof module<"u"?c(exports,require("react/jsx-runtime"),require("react")):typeof define=="function"&&define.amd?define(["exports","react/jsx-runtime","react"],c):(u=typeof globalThis<"u"?globalThis:u||self,c(u["react-imperial-modal"]={},u.jsxRuntime,u.React))})(this,(function(u,c,e){"use strict";const O=()=>{let l,n;return{promise:new Promise((f,d)=>{l=f,n=d}),resolve:l,reject:n}},q="Escape",A=["a[href]:not([tabindex='-1'])","area[href]:not([tabindex='-1'])","input:not([disabled]):not([tabindex='-1'])","select:not([disabled]):not([tabindex='-1'])","textarea:not([disabled]):not([tabindex='-1'])","button:not([disabled]):not([tabindex='-1'])","iframe:not([tabindex='-1'])","[tabindex]:not([tabindex='-1'])","[contentEditable=true]:not([tabindex='-1'])"].join(", "),T=function({className:l,entry:n}){const{role:a="dialog",label:f,labelledby:d,componentProps:r,Component:M}=n,s=e.useRef(null),{removeModal:b}=e.useContext(g);e.useLayoutEffect(()=>{requestAnimationFrame(()=>{var m;s.current&&(s.current.showModal(),(m=s.current.querySelector(A))==null||m.focus())})},[]);const v=e.useCallback(m=>{m.key===q&&!n.ignoreEscape&&b(n.instanceId)},[n,b]);return c.jsx("dialog",{ref:s,role:a,"aria-label":f,"aria-labelledby":d,className:l,onKeyDown:v,children:c.jsx(M,{close:n.closeModal,resolve:n.resolveModal,reject:n.rejectModal,...r})})},y=()=>{throw new Error("Attempted to call useModal outside of modal context. Make sure your component is inside ModalProvider.")},g=e.createContext({addModal:y,removeModal:y,resolveModal:y,rejectModal:y}),I={bodyOpenClass:"modal-open",modalContainerClass:"modals",modalClass:"modal"},k=document.documentElement,S=document.body,K=({children:l,config:n={}})=>{const[a,f]=e.useState([]),d=e.useMemo(()=>({...I,...n}),[n]),r=e.useRef(null),M=e.useRef([]),s=a.at(-1),b=e.useCallback(()=>{var o;S.classList.add(d.bodyOpenClass),(o=r==null?void 0:r.current)==null||o.setAttribute("aria-hidden","true"),k.style.overflow="hidden"},[d]),v=e.useCallback(()=>{var o;S.classList.remove(d.bodyOpenClass),(o=r==null?void 0:r.current)==null||o.removeAttribute("aria-hidden"),k.style.overflow=""},[d]),m=e.useCallback(o=>{f(t=>t.includes(o)?t:(t.length===0&&b(),M.current.push(document.activeElement),[...t,o]))},[b]),h=e.useCallback(o=>{f(t=>{const E=t.find(P=>P.instanceId===o),i=o===void 0?s:E,x=t.length===1,w=M.current.pop();return!i||!t.includes(i)?t:(x&&w&&k.contains(w)&&w.focus(),x&&v(),t.filter(P=>i!==P))})},[v,s]),p=e.useCallback((o,t)=>{const E=a.find(x=>x.instanceId===t),i=t===void 0?s:E;i==null||i.resolveModal(o||null)},[a,s]),C=e.useCallback((o,t)=>{const E=a.find(x=>x.instanceId===t),i=t===void 0?s:E;i==null||i.rejectModal(o||null)},[a,s]),j=e.useMemo(()=>({addModal:m,removeModal:h,resolveModal:p,rejectModal:C}),[m,h,p,C]);return c.jsxs(g.Provider,{value:j,children:[c.jsx("div",{ref:r,children:l}),c.jsx("div",{className:d.modalContainerClass,children:a.map(o=>c.jsx(T,{className:d.modalClass,entry:o},o.instanceId))})]})};let L=1;const D=()=>{const l=e.useContext(g);return[e.useCallback((a,f,d=!1,r,M,s="dialog")=>{const{promise:b,resolve:v,reject:m}=O(),h=(L++/5).toString(32),p=()=>l.removeModal(h),C=Object.assign(b,{instanceId:h,Component:a,componentProps:f,ignoreEscape:d,labelledby:M,label:r,role:s,resolveModal:j=>{v(j),p()},rejectModal:j=>{m(j),p()},closeModal:p});return l.addModal(C),C},[l]),l.resolveModal,l.rejectModal]};u.ModalProvider=K,u.useModal=D,Object.defineProperty(u,Symbol.toStringTag,{value:"Module"})}));
@@ -0,0 +1,33 @@
1
+ import { ReactNode } from 'react';
2
+ export interface ModalProps<T> {
3
+ resolve: (val: T | null) => void;
4
+ reject: (reason?: unknown) => void;
5
+ close: () => void;
6
+ }
7
+ export type ModalEntry<T, P> = Promise<T | null> & {
8
+ instanceId: string;
9
+ Component: React.ComponentType<P & ModalProps<T>>;
10
+ componentProps: P;
11
+ resolveModal: (val: T | null) => void;
12
+ rejectModal: (reason?: unknown) => void;
13
+ closeModal: () => void;
14
+ ignoreEscape?: boolean;
15
+ labelledby?: string;
16
+ label?: string;
17
+ role?: string;
18
+ };
19
+ export type ModalProviderConfig = {
20
+ bodyOpenClass?: string;
21
+ modalContainerClass?: string;
22
+ modalClass?: string;
23
+ };
24
+ export type ModalProviderProps = {
25
+ children?: ReactNode;
26
+ config?: ModalProviderConfig;
27
+ };
28
+ export type ModalContextValue = {
29
+ addModal: (entry: ModalEntry<unknown, unknown>) => void;
30
+ removeModal: (instanceId?: string) => void;
31
+ resolveModal: (val: unknown | null, instanceId?: string) => void;
32
+ rejectModal: (val: unknown | null, instanceId?: string) => void;
33
+ };
@@ -0,0 +1,2 @@
1
+ import { ModalEntry, ModalProps } from './types';
2
+ export declare const useModal: () => readonly [<T, P>(Component: React.ComponentType<P & ModalProps<T>>, componentProps: P, ignoreEscape?: boolean, label?: string, labelledby?: string, role?: string) => ModalEntry<T, P>, (val: unknown | null, instanceId?: string) => void, (val: unknown | null, instanceId?: string) => void];
package/package.json CHANGED
@@ -1,51 +1,66 @@
1
- {
2
- "name": "react-imperial-modal",
3
- "version": "1.0.5",
4
- "description": "imperative modal api for react",
5
- "author": "Greg Archer (greg.taff@gmail.com)",
6
- "license": "MIT",
7
- "homepage": "https://github.com/nihlton/react-imperial-modal#readme",
8
- "main": "build/index.js",
9
- "repository": {
10
- "type": "git",
11
- "url": "git+https://github.com/nihlton/react-imperial-modal.git"
12
- },
13
- "bugs": {
14
- "url": "https://github.com/nihlton/react-imperial-modal/issues"
15
- },
16
- "keywords": [
17
- "react",
18
- "modal",
19
- "imperative"
20
- ],
21
- "scripts": {
22
- "start": "webpack --watch",
23
- "build": "webpack"
24
- },
25
- "peerDependencies": {
26
- "react": "^16.13.1",
27
- "react-dom": "^16.13.1"
28
- },
29
- "devDependencies": {
30
- "@babel/core": "^7.4.5",
31
- "@babel/plugin-proposal-class-properties": "^7.4.4",
32
- "@babel/plugin-proposal-object-rest-spread": "^7.4.4",
33
- "@babel/plugin-transform-react-jsx": "^7.3.0",
34
- "@babel/preset-env": "^7.4.5",
35
- "@babel/preset-es2015": "^7.0.0-beta.53",
36
- "@babel/preset-react": "^7.0.0",
37
- "babel-cli": "^6.26.0",
38
- "babel-loader": "^8.0.6",
39
- "html-webpack-plugin": "^3.2.0",
40
- "webpack": "^4.35.0",
41
- "webpack-cli": "^3.0.4",
42
- "ts-loader": "^7.0.0"
43
- },
44
- "dependencies": {
45
- "@types/jest": "^25.2.2",
46
- "@types/node": "^14.0.1",
47
- "@types/react": "^16.9.35",
48
- "@types/react-dom": "^16.9.8",
49
- "typescript": "^3.9.2"
50
- }
51
- }
1
+ {
2
+ "name": "react-imperial-modal",
3
+ "version": "2.0.0",
4
+ "description": "imperative modal api for react",
5
+ "author": "Greg Archer (greg.taff@gmail.com)",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/nihlton/react-imperial-modal#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/nihlton/react-imperial-modal.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/nihlton/react-imperial-modal/issues"
14
+ },
15
+ "keywords": [
16
+ "react",
17
+ "modal",
18
+ "imperative"
19
+ ],
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "type": "module",
24
+ "main": "./dist/react-imperial-modal.umd.cjs",
25
+ "module": "./dist/react-imperial-modal.js",
26
+ "types": "./dist/react-imperial-modal.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/react-imperial-modal.d.ts",
30
+ "import": "./dist/react-imperial-modal.js",
31
+ "require": "./dist/react-imperial-modal.umd.cjs"
32
+ }
33
+ },
34
+ "peerDependencies": {
35
+ "react": ">=16.8.6",
36
+ "react-dom": ">=16.8.6"
37
+ },
38
+ "dependencies": {},
39
+ "scripts": {
40
+ "dev": "vite",
41
+ "build": "tsc && vite build",
42
+ "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
43
+ "lint:fix": "eslint . --fix",
44
+ "typecheck": "tsc --noEmit"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^25.0.3",
48
+ "@types/react": "^19.2.0",
49
+ "@types/react-dom": "^19.2.0",
50
+ "@vitejs/plugin-react": "^4.7.0",
51
+ "eslint": "^9.39.2",
52
+ "eslint-plugin-jsx-a11y": "^6.10.2",
53
+ "eslint-plugin-react": "^7.37.5",
54
+ "eslint-plugin-react-hooks": "^7.0.1",
55
+ "react": "^19.2.0",
56
+ "react-dom": "^19.2.0",
57
+ "sass": "^1.95.1",
58
+ "simpl-grid": "2.0.14",
59
+ "source-map-loader": "^0.2.4",
60
+ "ts-loader": "^7.0.0",
61
+ "typescript": "^5.9.3",
62
+ "typescript-eslint": "^8.50.1",
63
+ "vite": "^6.4.1",
64
+ "vite-plugin-dts": "^4.5.4"
65
+ }
66
+ }
package/.babelrc DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "presets": [ "@babel/preset-react", "@babel/preset-env"],
3
- "plugins": [
4
- "@babel/plugin-proposal-object-rest-spread",
5
- "@babel/plugin-transform-react-jsx",
6
- "@babel/plugin-proposal-class-properties"
7
- ]
8
- }
package/build/index.js DELETED
@@ -1,2 +0,0 @@
1
- module.exports=function(e){var t={};function n(o){if(t[o])return t[o].exports;var r=t[o]={i:o,l:!1,exports:{}};return e[o].call(r.exports,r,r.exports,n),r.l=!0,r.exports}return n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)n.d(o,r,function(t){return e[t]}.bind(null,r));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=2)}([function(e,t){e.exports=require("react")},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ModalContext=void 0;const o=n(0),r=e=>{throw new Error("Attempted to call useModal outside of modal context. Make sure your component is inside ModalProvider.")};t.ModalContext=o.createContext({addModal:r,removeModal:r})},function(e,t,n){"use strict";var o=this&&this.__createBinding||(Object.create?function(e,t,n,o){void 0===o&&(o=n),Object.defineProperty(e,o,{enumerable:!0,get:function(){return t[n]}})}:function(e,t,n,o){void 0===o&&(o=n),e[o]=t[n]}),r=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||t.hasOwnProperty(n)||o(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),r(n(3),t),r(n(5),t)},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ModalProvider=void 0;const o=n(0),r=n(1),l=n(4),{useRef:a,useState:d,useCallback:s,useMemo:i}=o,u={bodyOpenClass:"modal-open",modalShadeClass:"modal-shade",modalContainerClass:"modals",modalClass:"modal"},c=[];t.ModalProvider=({children:e,config:t={},appElement:n=(()=>{})})=>{const[f,m]=d([]),b=Object.assign(Object.assign({},u),t),v=a(),p=s(e=>{c.push(document.activeElement),0===f.length&&(()=>{const e=n()||(null==v?void 0:v.current);document.documentElement.style.overflow="hidden",document.body.classList.add(b.bodyOpenClass),e.setAttribute("aria-hidden","true")})(),f.includes(e)?console.warn("tried to open a modal that was already opened"):m(t=>[...t,e])},[f]),y=s(e=>{const t=c.pop();document.documentElement.contains(t)&&t.focus(),1===f.length&&(()=>{var e;const t=n()||(null==v?void 0:v.current);document.documentElement.style.overflow="",null===(e=null===document||void 0===document?void 0:document.body)||void 0===e||e.classList.remove(b.bodyOpenClass),t.removeAttribute("aria-hidden")})(),f.includes(e)?m(t=>{const n=[...t];return n.splice(n.indexOf(e),1),n}):console.warn("tried to close a modal that isn't open")},[f]),x=i(()=>({addModal:p,removeModal:y}),[f]),M=e=>{e.resolver(),y(e)};return o.createElement(r.ModalContext.Provider,{value:x},o.createElement(o.Fragment,null,o.createElement("div",{ref:v},e),f.length>0&&o.createElement("div",{className:b.modalContainerClass,onKeyDown:e=>{"Escape"===e.key&&M(f.slice(-1)[0])}},f.map((e,t)=>o.createElement(l.default,{key:"modal-"+t,className:b.modalClass,label:e.label,role:e.role},e.modal)),o.createElement("div",{className:b.modalShadeClass,onClick:()=>M(f.slice(-1)[0])}))))}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const o=n(0),{useRef:r,useEffect:l}=o,a=["a[href]:not([tabindex='-1'])","area[href]:not([tabindex='-1'])","input:not([disabled]):not([tabindex='-1'])","select:not([disabled]):not([tabindex='-1'])","textarea:not([disabled]):not([tabindex='-1'])","button:not([disabled]):not([tabindex='-1'])","iframe:not([tabindex='-1'])","[tabindex]:not([tabindex='-1'])","[contentEditable=true]:not([tabindex='-1'])"].join(", ");t.default=function(e){const{className:t,children:n,label:d,role:s}=e,i=r();l(()=>{u([0,1])},[i]);const u=e=>{var t;const n=(Array.from(null===(t=null==i?void 0:i.current)||void 0===t?void 0:t.querySelectorAll(a))||[]).slice(...e)[0];n&&n.focus()};return o.createElement(o.Fragment,null,o.createElement("div",{tabIndex:0,onFocus:()=>u([-1])}),o.createElement("div",{role:s,"aria-label":d,ref:i,className:t},n),o.createElement("div",{tabIndex:0,onFocus:()=>u([0,1])}))}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.useModal=t.usePrevious=void 0;const o=n(0),r=n(1);function l(e){const t=o.useRef();return o.useEffect(()=>{t.current=e},[e]),t.current}t.usePrevious=l,t.useModal=()=>{const[e,t]=o.useState([]),n=l(e)||[],a=o.useContext(r.ModalContext);o.useEffect(()=>{const t=e.filter(e=>!n.includes(e)),o=n.filter(t=>!e.includes(t));t.forEach(e=>{a.addModal(e)}),o.forEach(e=>{a.removeModal(e)})},[e]);return[(e,n="",o="dialog")=>{let r;const l=new Promise(e=>{r=e}),a={modal:e,resolver:r,label:n,role:o};return t(e=>[...e,a]),l},(e,n)=>{t(t=>{const o=t.find(t=>t.modal===e),r=[...t];return r.splice(r.indexOf(o),1),o.resolver(n),r})}]}}]);
2
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["webpack:///webpack/bootstrap","webpack:///external \"react\"","webpack:///./src/ModalContext.ts","webpack:///./src/index.ts","webpack:///./src/ModalProvider.tsx","webpack:///./src/Modal.tsx","webpack:///./src/useModal.ts"],"names":["installedModules","__webpack_require__","moduleId","exports","module","i","l","modules","call","m","c","d","name","getter","o","Object","defineProperty","enumerable","get","r","Symbol","toStringTag","value","t","mode","__esModule","ns","create","key","bind","n","object","property","prototype","hasOwnProperty","p","s","require","useWithoutProvider","modalEntry","Error","ModalContext","React","createContext","addModal","removeModal","useRef","useState","useCallback","useMemo","defaultConfig","bodyOpenClass","modalShadeClass","modalContainerClass","modalClass","previouslyFocusedElements","ModalProvider","children","config","appElement","modalEntries","setModalEntries","configuration","appContainer","push","document","activeElement","length","ariaTarget","current","documentElement","style","overflow","body","classList","add","setAttribute","modalSetup","includes","console","warn","openModals","previouslyFocusedElement","pop","contains","focus","remove","removeAttribute","modalTakeDown","newModals","splice","indexOf","contextValue","internalClose","entry","resolver","Provider","Fragment","ref","className","onKeyDown","event","slice","map","label","role","modal","onClick","useEffect","focusableSelector","join","props","modalRef","handleTabBoundary","indexes","element","Array","from","querySelectorAll","tabIndex","onFocus","usePrevious","useModal","localModalEntries","setLocalModalEntries","prevEntries","context","useContext","addedModals","filter","removedModals","forEach","modalPromise","Promise","resolve","currentModalEntries","result","thisEntry","find","newModalEntries"],"mappings":"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","file":"../build/index.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 2);\n","module.exports = require(\"react\");","import * as React from 'react'\r\nimport {ModalEntry} from \"./types\";\r\n\r\nconst useWithoutProvider = (modalEntry : ModalEntry) : void => {\r\n throw new Error( 'Attempted to call useModal outside of modal context. Make sure your component is inside ModalProvider.' )\r\n}\r\n\r\nexport const ModalContext = React.createContext({\r\n addModal: useWithoutProvider,\r\n removeModal: useWithoutProvider\r\n})\r\n","export * from './ModalProvider'\r\nexport * from './useModal'\r\n","import * as React from 'react'\r\nimport { ReactElement } from 'react'\r\nimport { ModalContext } from './ModalContext'\r\nimport Modal from './Modal'\r\n\r\nimport { ModalEntry, ModalProviderProps } from './types'\r\n\r\nconst { useRef, useState, useCallback, useMemo } = React\r\n\r\nconst defaultConfig = {\r\n bodyOpenClass: 'modal-open',\r\n modalShadeClass: 'modal-shade',\r\n modalContainerClass: 'modals',\r\n modalClass: 'modal'\r\n}\r\n\r\nconst previouslyFocusedElements : HTMLElement[] = []\r\nconst ESC_KEY = 'Escape'\r\n\r\n\r\nexport const ModalProvider = ({ children, config = {}, appElement = () => {} }: ModalProviderProps ) : ReactElement => {\r\n const [ modalEntries, setModalEntries ] = useState([])\r\n const configuration = {...defaultConfig, ...config}\r\n const appContainer = useRef()\r\n \r\n const modalSetup = () : void => {\r\n // showing first modal\r\n const ariaTarget = appElement() || appContainer?.current\r\n document.documentElement.style.overflow = 'hidden'\r\n document.body.classList.add(configuration.bodyOpenClass)\r\n ariaTarget.setAttribute('aria-hidden', 'true');\r\n }\r\n \r\n const modalTakeDown = () : void => {\r\n // removing last modal\r\n const ariaTarget = appElement() || appContainer?.current\r\n document.documentElement.style.overflow = ''\r\n document?.body?.classList.remove(configuration.bodyOpenClass)\r\n ariaTarget.removeAttribute('aria-hidden');\r\n }\r\n \r\n const addModal = useCallback((modalEntry : ModalEntry) : void => {\r\n // remember where focus was\r\n previouslyFocusedElements.push(document.activeElement as HTMLInputElement)\r\n \r\n if (modalEntries.length === 0) {\r\n modalSetup()\r\n }\r\n \r\n if (modalEntries.includes(modalEntry)) {\r\n console.warn('tried to open a modal that was already opened')\r\n } else {\r\n setModalEntries(openModals => [...openModals, modalEntry ])\r\n }\r\n }, [modalEntries])\r\n \r\n const removeModal = useCallback((modalEntry : ModalEntry) : void => {\r\n // set focus back\r\n const previouslyFocusedElement:HTMLElement = previouslyFocusedElements.pop()\r\n if (document.documentElement.contains(previouslyFocusedElement)) {\r\n previouslyFocusedElement.focus()\r\n }\r\n \r\n if (modalEntries.length === 1) {\r\n modalTakeDown()\r\n }\r\n \r\n if (modalEntries.includes(modalEntry)) {\r\n setModalEntries(openModals => {\r\n const newModals = [...openModals]\r\n newModals.splice(newModals.indexOf(modalEntry), 1)\r\n return newModals\r\n })\r\n } else {\r\n console.warn(`tried to close a modal that isn't open`)\r\n }\r\n }, [modalEntries])\r\n \r\n const contextValue = useMemo(() => ({ addModal, removeModal }), [modalEntries]);\r\n const handleKey = (event : React.KeyboardEvent) : void => {\r\n if (event.key === ESC_KEY) {\r\n internalClose(modalEntries.slice(-1)[0])\r\n }\r\n }\r\n\r\n const internalClose = (entry: ModalEntry) : void => {\r\n entry.resolver()\r\n removeModal(entry)\r\n }\r\n \r\n return <ModalContext.Provider value={contextValue}>\r\n <React.Fragment>\r\n <div ref={appContainer}>{children}</div>\r\n {modalEntries.length > 0 && <div className={configuration.modalContainerClass} onKeyDown={handleKey}>\r\n {modalEntries.map((modalEntry, i) => (\r\n <Modal\r\n key={`modal-${i}`}\r\n className={configuration.modalClass}\r\n label={modalEntry.label}\r\n role={modalEntry.role}\r\n >{modalEntry.modal}</Modal>\r\n ))}\r\n <div className={configuration.modalShadeClass} onClick={() => internalClose(modalEntries.slice(-1)[0])} />\r\n </div>}\r\n </React.Fragment>\r\n </ModalContext.Provider>\r\n}\r\n","import * as React from 'react'\r\nimport {ReactChildren, ReactElement} from \"react\";\r\nconst { useRef, useEffect } = React\r\n\r\nconst focusableSelector = [\r\n 'a[href]:not([tabindex=\\'-1\\'])',\r\n 'area[href]:not([tabindex=\\'-1\\'])',\r\n 'input:not([disabled]):not([tabindex=\\'-1\\'])',\r\n 'select:not([disabled]):not([tabindex=\\'-1\\'])',\r\n 'textarea:not([disabled]):not([tabindex=\\'-1\\'])',\r\n 'button:not([disabled]):not([tabindex=\\'-1\\'])',\r\n 'iframe:not([tabindex=\\'-1\\'])',\r\n '[tabindex]:not([tabindex=\\'-1\\'])',\r\n '[contentEditable=true]:not([tabindex=\\'-1\\'])'\r\n].join(', ')\r\n\r\ntype ModalProps = {\r\n className?: string,\r\n children?: ReactChildren,\r\n label?: string,\r\n role?: string,\r\n}\r\n\r\nconst Modal = function (props : ModalProps): ReactElement {\r\n const { className, children, label, role } = props\r\n const modalRef = useRef<HTMLDivElement>()\r\n \r\n useEffect(() => {\r\n handleTabBoundary([0, 1])\r\n }, [ modalRef ])\r\n \r\n const handleTabBoundary = (indexes : number[]) : void => {\r\n const focusableElements = Array.from(modalRef?.current?.querySelectorAll<HTMLElement>(focusableSelector)) || []\r\n const element = focusableElements.slice(...indexes)[0]\r\n \r\n if (element) { element.focus() }\r\n }\r\n \r\n return <React.Fragment>\r\n <div tabIndex={0} onFocus={() => handleTabBoundary([-1])}/>\r\n <div\r\n role={role}\r\n aria-label={label}\r\n ref={modalRef}\r\n className={className}>{children}</div>\r\n <div tabIndex={0} onFocus={() => handleTabBoundary([0, 1])}/>\r\n </React.Fragment>\r\n}\r\n\r\nexport default Modal\r\n","import {useRef, useState, useEffect, useContext, ReactElement} from 'react'\r\nimport { ModalContext } from './ModalContext'\r\nimport {ModalEntry} from \"./types\";\r\n\r\nexport function usePrevious(value : any) : any {\r\n const ref = useRef()\r\n useEffect(() => { ref.current = value }, [value])\r\n \r\n return ref.current\r\n}\r\n\r\nexport const useModal = () => {\r\n const [ localModalEntries, setLocalModalEntries ] = useState([])\r\n const prevEntries: ModalEntry[] = usePrevious(localModalEntries) || []\r\n \r\n const context = useContext(ModalContext);\r\n \r\n useEffect(() => {\r\n const addedModals = localModalEntries.filter(modalEntry => !prevEntries.includes(modalEntry))\r\n const removedModals = prevEntries.filter(modalEntry => !localModalEntries.includes(modalEntry))\r\n \r\n addedModals.forEach(modalEntry => { context.addModal(modalEntry) })\r\n removedModals.forEach(modalEntry => { context.removeModal(modalEntry) })\r\n }, [ localModalEntries ])\r\n \r\n const open = (modal : ReactElement, label: string = '', role: string = 'dialog') : Promise<any> => {\r\n let resolver: () => any\r\n const modalPromise = new Promise((resolve) => { resolver = resolve })\r\n const modalEntry = {modal, resolver, label, role}\r\n setLocalModalEntries(currentModalEntries => [...currentModalEntries, modalEntry ])\r\n \r\n return modalPromise\r\n }\r\n \r\n const close = (modal: ReactElement, result: any) : void => {\r\n setLocalModalEntries(currentModalEntries => {\r\n const thisEntry = currentModalEntries.find(entry => entry.modal === modal)\r\n const newModalEntries = [...currentModalEntries]\r\n newModalEntries.splice(newModalEntries.indexOf(thisEntry), 1)\r\n thisEntry.resolver(result)\r\n return newModalEntries\r\n })\r\n }\r\n \r\n return [ open, close ]\r\n}\r\n"],"sourceRoot":""}
package/src/Modal.tsx DELETED
@@ -1,50 +0,0 @@
1
- import * as React from 'react'
2
- import {ReactChildren, ReactElement} from "react";
3
- const { useRef, useEffect } = React
4
-
5
- const focusableSelector = [
6
- 'a[href]:not([tabindex=\'-1\'])',
7
- 'area[href]:not([tabindex=\'-1\'])',
8
- 'input:not([disabled]):not([tabindex=\'-1\'])',
9
- 'select:not([disabled]):not([tabindex=\'-1\'])',
10
- 'textarea:not([disabled]):not([tabindex=\'-1\'])',
11
- 'button:not([disabled]):not([tabindex=\'-1\'])',
12
- 'iframe:not([tabindex=\'-1\'])',
13
- '[tabindex]:not([tabindex=\'-1\'])',
14
- '[contentEditable=true]:not([tabindex=\'-1\'])'
15
- ].join(', ')
16
-
17
- type ModalProps = {
18
- className?: string,
19
- children?: ReactChildren,
20
- label?: string,
21
- role?: string,
22
- }
23
-
24
- const Modal = function (props : ModalProps): ReactElement {
25
- const { className, children, label, role } = props
26
- const modalRef = useRef<HTMLDivElement>()
27
-
28
- useEffect(() => {
29
- handleTabBoundary([0, 1])
30
- }, [ modalRef ])
31
-
32
- const handleTabBoundary = (indexes : number[]) : void => {
33
- const focusableElements = Array.from(modalRef?.current?.querySelectorAll<HTMLElement>(focusableSelector)) || []
34
- const element = focusableElements.slice(...indexes)[0]
35
-
36
- if (element) { element.focus() }
37
- }
38
-
39
- return <React.Fragment>
40
- <div tabIndex={0} onFocus={() => handleTabBoundary([-1])}/>
41
- <div
42
- role={role}
43
- aria-label={label}
44
- ref={modalRef}
45
- className={className}>{children}</div>
46
- <div tabIndex={0} onFocus={() => handleTabBoundary([0, 1])}/>
47
- </React.Fragment>
48
- }
49
-
50
- export default Modal
@@ -1,11 +0,0 @@
1
- import * as React from 'react'
2
- import {ModalEntry} from "./types";
3
-
4
- const useWithoutProvider = (modalEntry : ModalEntry) : void => {
5
- throw new Error( 'Attempted to call useModal outside of modal context. Make sure your component is inside ModalProvider.' )
6
- }
7
-
8
- export const ModalContext = React.createContext({
9
- addModal: useWithoutProvider,
10
- removeModal: useWithoutProvider
11
- })
@@ -1,107 +0,0 @@
1
- import * as React from 'react'
2
- import { ReactElement } from 'react'
3
- import { ModalContext } from './ModalContext'
4
- import Modal from './Modal'
5
-
6
- import { ModalEntry, ModalProviderProps } from './types'
7
-
8
- const { useRef, useState, useCallback, useMemo } = React
9
-
10
- const defaultConfig = {
11
- bodyOpenClass: 'modal-open',
12
- modalShadeClass: 'modal-shade',
13
- modalContainerClass: 'modals',
14
- modalClass: 'modal'
15
- }
16
-
17
- const previouslyFocusedElements : HTMLElement[] = []
18
- const ESC_KEY = 'Escape'
19
-
20
-
21
- export const ModalProvider = ({ children, config = {}, appElement = () => {} }: ModalProviderProps ) : ReactElement => {
22
- const [ modalEntries, setModalEntries ] = useState([])
23
- const configuration = {...defaultConfig, ...config}
24
- const appContainer = useRef()
25
-
26
- const modalSetup = () : void => {
27
- // showing first modal
28
- const ariaTarget = appElement() || appContainer?.current
29
- document.documentElement.style.overflow = 'hidden'
30
- document.body.classList.add(configuration.bodyOpenClass)
31
- ariaTarget.setAttribute('aria-hidden', 'true');
32
- }
33
-
34
- const modalTakeDown = () : void => {
35
- // removing last modal
36
- const ariaTarget = appElement() || appContainer?.current
37
- document.documentElement.style.overflow = ''
38
- document?.body?.classList.remove(configuration.bodyOpenClass)
39
- ariaTarget.removeAttribute('aria-hidden');
40
- }
41
-
42
- const addModal = useCallback((modalEntry : ModalEntry) : void => {
43
- // remember where focus was
44
- previouslyFocusedElements.push(document.activeElement as HTMLInputElement)
45
-
46
- if (modalEntries.length === 0) {
47
- modalSetup()
48
- }
49
-
50
- if (modalEntries.includes(modalEntry)) {
51
- console.warn('tried to open a modal that was already opened')
52
- } else {
53
- setModalEntries(openModals => [...openModals, modalEntry ])
54
- }
55
- }, [modalEntries])
56
-
57
- const removeModal = useCallback((modalEntry : ModalEntry) : void => {
58
- // set focus back
59
- const previouslyFocusedElement:HTMLElement = previouslyFocusedElements.pop()
60
- if (document.documentElement.contains(previouslyFocusedElement)) {
61
- previouslyFocusedElement.focus()
62
- }
63
-
64
- if (modalEntries.length === 1) {
65
- modalTakeDown()
66
- }
67
-
68
- if (modalEntries.includes(modalEntry)) {
69
- setModalEntries(openModals => {
70
- const newModals = [...openModals]
71
- newModals.splice(newModals.indexOf(modalEntry), 1)
72
- return newModals
73
- })
74
- } else {
75
- console.warn(`tried to close a modal that isn't open`)
76
- }
77
- }, [modalEntries])
78
-
79
- const contextValue = useMemo(() => ({ addModal, removeModal }), [modalEntries]);
80
- const handleKey = (event : React.KeyboardEvent) : void => {
81
- if (event.key === ESC_KEY) {
82
- internalClose(modalEntries.slice(-1)[0])
83
- }
84
- }
85
-
86
- const internalClose = (entry: ModalEntry) : void => {
87
- entry.resolver()
88
- removeModal(entry)
89
- }
90
-
91
- return <ModalContext.Provider value={contextValue}>
92
- <React.Fragment>
93
- <div ref={appContainer}>{children}</div>
94
- {modalEntries.length > 0 && <div className={configuration.modalContainerClass} onKeyDown={handleKey}>
95
- {modalEntries.map((modalEntry, i) => (
96
- <Modal
97
- key={`modal-${i}`}
98
- className={configuration.modalClass}
99
- label={modalEntry.label}
100
- role={modalEntry.role}
101
- >{modalEntry.modal}</Modal>
102
- ))}
103
- <div className={configuration.modalShadeClass} onClick={() => internalClose(modalEntries.slice(-1)[0])} />
104
- </div>}
105
- </React.Fragment>
106
- </ModalContext.Provider>
107
- }
package/src/index.ts DELETED
@@ -1,2 +0,0 @@
1
- export * from './ModalProvider'
2
- export * from './useModal'
package/src/types.ts DELETED
@@ -1,21 +0,0 @@
1
- import {ReactChildren, ReactElement} from 'react'
2
-
3
- export type ModalEntry = {
4
- modal: ReactElement,
5
- resolver: () => Promise<any>,
6
- label?: string,
7
- role?: string
8
- }
9
-
10
- export type ModalProviderConfig = {
11
- bodyOpenClass?: string
12
- modalShadeClass?: string
13
- modalContainerClass?: string
14
- modalClass?: string
15
- }
16
-
17
- export type ModalProviderProps = {
18
- children?: ReactChildren
19
- config?: ModalProviderConfig
20
- appElement?: () => HTMLElement | void
21
- }
package/src/useModal.ts DELETED
@@ -1,46 +0,0 @@
1
- import {useRef, useState, useEffect, useContext, ReactElement} from 'react'
2
- import { ModalContext } from './ModalContext'
3
- import {ModalEntry} from "./types";
4
-
5
- export function usePrevious(value : any) : any {
6
- const ref = useRef()
7
- useEffect(() => { ref.current = value }, [value])
8
-
9
- return ref.current
10
- }
11
-
12
- export const useModal = () => {
13
- const [ localModalEntries, setLocalModalEntries ] = useState([])
14
- const prevEntries: ModalEntry[] = usePrevious(localModalEntries) || []
15
-
16
- const context = useContext(ModalContext);
17
-
18
- useEffect(() => {
19
- const addedModals = localModalEntries.filter(modalEntry => !prevEntries.includes(modalEntry))
20
- const removedModals = prevEntries.filter(modalEntry => !localModalEntries.includes(modalEntry))
21
-
22
- addedModals.forEach(modalEntry => { context.addModal(modalEntry) })
23
- removedModals.forEach(modalEntry => { context.removeModal(modalEntry) })
24
- }, [ localModalEntries ])
25
-
26
- const open = (modal : ReactElement, label: string = '', role: string = 'dialog') : Promise<any> => {
27
- let resolver: () => any
28
- const modalPromise = new Promise((resolve) => { resolver = resolve })
29
- const modalEntry = {modal, resolver, label, role}
30
- setLocalModalEntries(currentModalEntries => [...currentModalEntries, modalEntry ])
31
-
32
- return modalPromise
33
- }
34
-
35
- const close = (modal: ReactElement, result: any) : void => {
36
- setLocalModalEntries(currentModalEntries => {
37
- const thisEntry = currentModalEntries.find(entry => entry.modal === modal)
38
- const newModalEntries = [...currentModalEntries]
39
- newModalEntries.splice(newModalEntries.indexOf(thisEntry), 1)
40
- thisEntry.resolver(result)
41
- return newModalEntries
42
- })
43
- }
44
-
45
- return [ open, close ]
46
- }
package/tsconfig.json DELETED
@@ -1,10 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "outDir": "./build/",
4
- "sourceMap": true,
5
- "noImplicitAny": true,
6
- "module": "commonjs",
7
- "target": "es6",
8
- "jsx": "react"
9
- }
10
- }
package/webpack.config.js DELETED
@@ -1,49 +0,0 @@
1
- const path = require('path');
2
-
3
- module.exports = {
4
- mode: 'production',
5
- entry: './src/index.ts',
6
- output: {
7
- path: path.resolve(__dirname, 'build'),
8
- filename: '../build/index.js',
9
- libraryTarget: 'commonjs2'
10
- },
11
- devtool: 'source-map',
12
- resolve: {
13
- // Add '.ts' and '.tsx' as resolvable extensions.
14
- extensions: ['.webpack.js', '.web.js', '.ts', '.tsx', '.js'],
15
- },
16
- module: {
17
- rules: [
18
- {
19
- test: /\.ts(x?)$/,
20
- exclude: /node_modules/,
21
- use: [
22
- {
23
- loader: "ts-loader"
24
- }
25
- ]
26
- },
27
- {
28
- enforce: "pre",
29
- test: /\.js$/,
30
- loader: "source-map-loader"
31
- },
32
- {
33
- test: /\.js$/,
34
- include: path.resolve(__dirname, 'src'),
35
- exclude: /(node_modules|bower_components|build)/,
36
- use: {
37
- loader: 'babel-loader',
38
- options: {
39
- presets: ["@babel/env"],
40
- }
41
- }
42
- },
43
- ]
44
- },
45
- externals: {
46
- 'react': 'commonjs react',
47
- 'react-dom': 'commonjs react-dom'
48
- }
49
- };