@violice/rmu 0.2.7 → 0.2.9
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 +341 -36
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +34 -0
- package/dist/index.d.mts +13 -28
- package/dist/index.mjs +2 -204
- package/dist/index.mjs.map +1 -0
- package/package.json +27 -19
- package/src/constants.ts +8 -0
- package/src/emitter.test.ts +85 -0
- package/src/emitter.ts +24 -16
- package/src/events.test.ts +59 -0
- package/src/events.ts +14 -13
- package/src/integration.test.tsx +266 -0
- package/src/rmu-outlet.test.tsx +213 -0
- package/src/rmu-outlet.tsx +3 -2
- package/src/rmu-provider.test.tsx +15 -0
- package/src/types.ts +20 -16
- package/src/use-rmu-events.test.ts +117 -0
- package/src/use-rmu-events.ts +5 -5
- package/src/use-rmu-state.test.ts +129 -0
- package/dist/index.d.ts +0 -49
- package/dist/index.js +0 -231
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { renderHook, act } from '@testing-library/react';
|
|
4
|
+
import { useRMUState } from './use-rmu-state';
|
|
5
|
+
|
|
6
|
+
describe('useRMUState', () => {
|
|
7
|
+
it('should initialize with empty outlets', () => {
|
|
8
|
+
const { result } = renderHook(() => useRMUState());
|
|
9
|
+
expect(result.current.outlets).toEqual({});
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should add outlet', () => {
|
|
13
|
+
const { result } = renderHook(() => useRMUState());
|
|
14
|
+
|
|
15
|
+
act(() => {
|
|
16
|
+
result.current.addOutlet('test-outlet');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
expect(result.current.outlets).toHaveProperty('test-outlet');
|
|
20
|
+
expect(result.current.outlets['test-outlet']).toEqual({});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should throw when adding duplicate outlet', () => {
|
|
24
|
+
const { result } = renderHook(() => useRMUState());
|
|
25
|
+
|
|
26
|
+
act(() => {
|
|
27
|
+
result.current.addOutlet('test-outlet');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(() => {
|
|
31
|
+
act(() => {
|
|
32
|
+
result.current.addOutlet('test-outlet');
|
|
33
|
+
});
|
|
34
|
+
}).toThrow('Outlet with id test-outlet already exists');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should remove outlet', () => {
|
|
38
|
+
const { result } = renderHook(() => useRMUState());
|
|
39
|
+
|
|
40
|
+
act(() => {
|
|
41
|
+
result.current.addOutlet('test-outlet');
|
|
42
|
+
result.current.removeOutlet('test-outlet');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(result.current.outlets).not.toHaveProperty('test-outlet');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should open modal in outlet', () => {
|
|
49
|
+
const { result } = renderHook(() => useRMUState());
|
|
50
|
+
const mockComponent = React.createElement('div', null, 'Test');
|
|
51
|
+
|
|
52
|
+
act(() => {
|
|
53
|
+
result.current.addOutlet('test-outlet');
|
|
54
|
+
result.current.openModal({
|
|
55
|
+
modalId: 'modal-1',
|
|
56
|
+
modalComponent: mockComponent,
|
|
57
|
+
outletId: 'test-outlet',
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(result.current.outlets['test-outlet']).toHaveProperty('modal-1');
|
|
62
|
+
expect(result.current.outlets['test-outlet']['modal-1']).toBe(mockComponent);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should throw when opening modal in non-existent outlet', () => {
|
|
66
|
+
const { result } = renderHook(() => useRMUState());
|
|
67
|
+
|
|
68
|
+
expect(() => {
|
|
69
|
+
act(() => {
|
|
70
|
+
result.current.openModal({
|
|
71
|
+
modalId: 'modal-1',
|
|
72
|
+
modalComponent: React.createElement('div', null, 'Test'),
|
|
73
|
+
outletId: 'non-existent',
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}).toThrow('Outlet with id non-existent not found');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should close modal', () => {
|
|
80
|
+
const { result } = renderHook(() => useRMUState());
|
|
81
|
+
|
|
82
|
+
act(() => {
|
|
83
|
+
result.current.addOutlet('test-outlet');
|
|
84
|
+
result.current.openModal({
|
|
85
|
+
modalId: 'modal-1',
|
|
86
|
+
modalComponent: React.createElement('div', null, 'Test'),
|
|
87
|
+
outletId: 'test-outlet',
|
|
88
|
+
});
|
|
89
|
+
result.current.closeModal({ modalId: 'modal-1', outletId: 'test-outlet' });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(result.current.outlets['test-outlet']).not.toHaveProperty('modal-1');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should throw when closing modal in non-existent outlet', () => {
|
|
96
|
+
const { result } = renderHook(() => useRMUState());
|
|
97
|
+
|
|
98
|
+
expect(() => {
|
|
99
|
+
act(() => {
|
|
100
|
+
result.current.closeModal({
|
|
101
|
+
modalId: 'modal-1',
|
|
102
|
+
outletId: 'non-existent',
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}).toThrow('Outlet with id non-existent not found');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should handle multiple modals in same outlet', () => {
|
|
109
|
+
const { result } = renderHook(() => useRMUState());
|
|
110
|
+
|
|
111
|
+
act(() => {
|
|
112
|
+
result.current.addOutlet('test-outlet');
|
|
113
|
+
result.current.openModal({
|
|
114
|
+
modalId: 'modal-1',
|
|
115
|
+
modalComponent: React.createElement('div', null, 'Modal 1'),
|
|
116
|
+
outletId: 'test-outlet',
|
|
117
|
+
});
|
|
118
|
+
result.current.openModal({
|
|
119
|
+
modalId: 'modal-2',
|
|
120
|
+
modalComponent: React.createElement('div', null, 'Modal 2'),
|
|
121
|
+
outletId: 'test-outlet',
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(Object.keys(result.current.outlets['test-outlet'])).toHaveLength(2);
|
|
126
|
+
expect(result.current.outlets['test-outlet']).toHaveProperty('modal-1');
|
|
127
|
+
expect(result.current.outlets['test-outlet']).toHaveProperty('modal-2');
|
|
128
|
+
});
|
|
129
|
+
});
|
package/dist/index.d.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import React, { ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
//#region src/rmu-provider.d.ts
|
|
4
|
-
declare const RMUProvider: ({
|
|
5
|
-
children
|
|
6
|
-
}: {
|
|
7
|
-
children: React.ReactNode;
|
|
8
|
-
}) => React.JSX.Element;
|
|
9
|
-
//#endregion
|
|
10
|
-
//#region src/rmu-outlet.d.ts
|
|
11
|
-
declare const RMUOutlet: ({
|
|
12
|
-
outletId
|
|
13
|
-
}: {
|
|
14
|
-
outletId?: string | undefined;
|
|
15
|
-
}) => React.JSX.Element;
|
|
16
|
-
//#endregion
|
|
17
|
-
//#region src/types.d.ts
|
|
18
|
-
type RMUContextState = {
|
|
19
|
-
outlets: Record<string, Record<string, ReactNode>>;
|
|
20
|
-
openModal: ({
|
|
21
|
-
modalId,
|
|
22
|
-
modalComponent,
|
|
23
|
-
outletId
|
|
24
|
-
}: {
|
|
25
|
-
modalId: string;
|
|
26
|
-
modalComponent: ReactNode;
|
|
27
|
-
outletId: string;
|
|
28
|
-
}) => void;
|
|
29
|
-
closeModal: ({
|
|
30
|
-
modalId,
|
|
31
|
-
outletId
|
|
32
|
-
}: {
|
|
33
|
-
modalId: string;
|
|
34
|
-
outletId: string;
|
|
35
|
-
}) => void;
|
|
36
|
-
addOutlet: (outletId: string) => void;
|
|
37
|
-
removeOutlet: (outletId: string) => void;
|
|
38
|
-
};
|
|
39
|
-
//#endregion
|
|
40
|
-
//#region src/events.d.ts
|
|
41
|
-
declare const openModal: (modalComponent: ReactNode, config?: {
|
|
42
|
-
outletId?: string;
|
|
43
|
-
}) => {
|
|
44
|
-
modalId: string;
|
|
45
|
-
outletId: string;
|
|
46
|
-
};
|
|
47
|
-
declare const closeModal: RMUContextState['closeModal'];
|
|
48
|
-
//#endregion
|
|
49
|
-
export { RMUOutlet, RMUProvider, closeModal, openModal };
|
package/dist/index.js
DELETED
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
//#region rolldown:runtime
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __copyProps = (to, from, except, desc) => {
|
|
9
|
-
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
-
key = keys[i];
|
|
11
|
-
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
-
get: ((k) => from[k]).bind(null, key),
|
|
13
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
-
value: mod,
|
|
20
|
-
enumerable: true
|
|
21
|
-
}) : target, mod));
|
|
22
|
-
|
|
23
|
-
//#endregion
|
|
24
|
-
let react = require("react");
|
|
25
|
-
react = __toESM(react);
|
|
26
|
-
|
|
27
|
-
//#region src/rmu-context.ts
|
|
28
|
-
const RMUContext = (0, react.createContext)(null);
|
|
29
|
-
|
|
30
|
-
//#endregion
|
|
31
|
-
//#region src/emitter.ts
|
|
32
|
-
const listenersByType = {};
|
|
33
|
-
const emitter = {
|
|
34
|
-
subscribe(type, handler) {
|
|
35
|
-
if (!listenersByType[type]) listenersByType[type] = /* @__PURE__ */ new Set();
|
|
36
|
-
listenersByType[type].add(handler);
|
|
37
|
-
},
|
|
38
|
-
unsubscribe(type, handler) {
|
|
39
|
-
const listeners = listenersByType[type];
|
|
40
|
-
if (listeners) {
|
|
41
|
-
listeners.delete(handler);
|
|
42
|
-
if (listeners.size === 0) delete listenersByType[type];
|
|
43
|
-
}
|
|
44
|
-
},
|
|
45
|
-
emit(type, payload) {
|
|
46
|
-
const listeners = listenersByType[type];
|
|
47
|
-
if (!listeners) return;
|
|
48
|
-
[...listeners].forEach((handler) => {
|
|
49
|
-
try {
|
|
50
|
-
handler(payload);
|
|
51
|
-
} catch {}
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
//#endregion
|
|
57
|
-
//#region src/events.ts
|
|
58
|
-
const RMU_EVENTS = {
|
|
59
|
-
open: "rmu:open-modal",
|
|
60
|
-
close: "rmu:close-modal"
|
|
61
|
-
};
|
|
62
|
-
const openModal = (modalComponent, config = {}) => {
|
|
63
|
-
const modalId = `rmu-modal-${(/* @__PURE__ */ new Date()).getTime().toString()}`;
|
|
64
|
-
const { outletId = "rmu-default-outlet" } = config;
|
|
65
|
-
emitter.emit(RMU_EVENTS.open, {
|
|
66
|
-
modalId,
|
|
67
|
-
modalComponent,
|
|
68
|
-
outletId
|
|
69
|
-
});
|
|
70
|
-
return {
|
|
71
|
-
modalId,
|
|
72
|
-
outletId
|
|
73
|
-
};
|
|
74
|
-
};
|
|
75
|
-
const closeModal = ({ modalId, outletId }) => {
|
|
76
|
-
emitter.emit(RMU_EVENTS.close, {
|
|
77
|
-
modalId,
|
|
78
|
-
outletId
|
|
79
|
-
});
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
//#endregion
|
|
83
|
-
//#region src/use-rmu-events.ts
|
|
84
|
-
const useRMUEvents = (ctx) => {
|
|
85
|
-
const events = {
|
|
86
|
-
open: (payload) => ctx.openModal(payload),
|
|
87
|
-
close: (payload) => ctx.closeModal(payload)
|
|
88
|
-
};
|
|
89
|
-
(0, react.useEffect)(() => {
|
|
90
|
-
Object.keys(events).forEach((event) => {
|
|
91
|
-
emitter.subscribe(RMU_EVENTS[event], events[event]);
|
|
92
|
-
});
|
|
93
|
-
return () => {
|
|
94
|
-
Object.keys(events).forEach((event) => {
|
|
95
|
-
emitter.unsubscribe(RMU_EVENTS[event], events[event]);
|
|
96
|
-
});
|
|
97
|
-
};
|
|
98
|
-
}, []);
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
//#endregion
|
|
102
|
-
//#region src/use-rmu-state.ts
|
|
103
|
-
const ACTIONS = {
|
|
104
|
-
openModal: "rmu:open-modal",
|
|
105
|
-
closeModal: "rmu:close-modal",
|
|
106
|
-
addOutlet: "rmu:add-modal",
|
|
107
|
-
removeOutlet: "rmu:remove-outlet"
|
|
108
|
-
};
|
|
109
|
-
const reducer = (state, action) => {
|
|
110
|
-
switch (action.type) {
|
|
111
|
-
case ACTIONS.openModal: {
|
|
112
|
-
const { modalId, modalComponent, outletId } = action.payload;
|
|
113
|
-
const modalOutlet = state.outlets[outletId];
|
|
114
|
-
if (!modalOutlet) throw new Error(`Outlet with id ${outletId} not found`);
|
|
115
|
-
return {
|
|
116
|
-
...state,
|
|
117
|
-
outlets: {
|
|
118
|
-
...state.outlets,
|
|
119
|
-
[outletId]: {
|
|
120
|
-
...modalOutlet,
|
|
121
|
-
[modalId]: modalComponent
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
case ACTIONS.closeModal: {
|
|
127
|
-
const { modalId, outletId } = action.payload;
|
|
128
|
-
const modalOutlet = state.outlets[outletId];
|
|
129
|
-
if (!modalOutlet) throw new Error(`Outlet with id ${outletId} not found`);
|
|
130
|
-
const { [modalId]: _removed,...restModals } = modalOutlet;
|
|
131
|
-
return {
|
|
132
|
-
...state,
|
|
133
|
-
outlets: {
|
|
134
|
-
...state.outlets,
|
|
135
|
-
[outletId]: restModals
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
case ACTIONS.addOutlet: {
|
|
140
|
-
const { outletId } = action.payload;
|
|
141
|
-
if (!!state.outlets[outletId]) throw new Error(`Outlet with id ${outletId} already exists`);
|
|
142
|
-
return {
|
|
143
|
-
...state,
|
|
144
|
-
outlets: {
|
|
145
|
-
...state.outlets,
|
|
146
|
-
[outletId]: {}
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
case ACTIONS.removeOutlet: {
|
|
151
|
-
const { outletId } = action.payload;
|
|
152
|
-
const { [outletId]: _removed,...restOutlets } = state.outlets;
|
|
153
|
-
return {
|
|
154
|
-
...state,
|
|
155
|
-
outlets: restOutlets
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
default: return state;
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
const useRMUState = () => {
|
|
162
|
-
const [state, dispatch] = (0, react.useReducer)(reducer, { outlets: {} });
|
|
163
|
-
const openModal$1 = ({ modalId, modalComponent, outletId }) => {
|
|
164
|
-
dispatch({
|
|
165
|
-
type: ACTIONS.openModal,
|
|
166
|
-
payload: {
|
|
167
|
-
modalId,
|
|
168
|
-
modalComponent,
|
|
169
|
-
outletId
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
};
|
|
173
|
-
const closeModal$1 = ({ modalId, outletId }) => {
|
|
174
|
-
dispatch({
|
|
175
|
-
type: ACTIONS.closeModal,
|
|
176
|
-
payload: {
|
|
177
|
-
modalId,
|
|
178
|
-
outletId
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
};
|
|
182
|
-
const addOutlet = (outletId) => {
|
|
183
|
-
dispatch({
|
|
184
|
-
type: ACTIONS.addOutlet,
|
|
185
|
-
payload: { outletId }
|
|
186
|
-
});
|
|
187
|
-
};
|
|
188
|
-
const removeOutlet = (outletId) => {
|
|
189
|
-
dispatch({
|
|
190
|
-
type: ACTIONS.removeOutlet,
|
|
191
|
-
payload: { outletId }
|
|
192
|
-
});
|
|
193
|
-
};
|
|
194
|
-
return {
|
|
195
|
-
...state,
|
|
196
|
-
openModal: openModal$1,
|
|
197
|
-
closeModal: closeModal$1,
|
|
198
|
-
addOutlet,
|
|
199
|
-
removeOutlet
|
|
200
|
-
};
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
//#endregion
|
|
204
|
-
//#region src/rmu-provider.tsx
|
|
205
|
-
const RMUProvider = ({ children }) => {
|
|
206
|
-
const state = useRMUState();
|
|
207
|
-
useRMUEvents(state);
|
|
208
|
-
return /* @__PURE__ */ react.default.createElement(RMUContext.Provider, { value: state }, children);
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
//#endregion
|
|
212
|
-
//#region src/rmu-outlet.tsx
|
|
213
|
-
const RMUOutlet = ({ outletId = "rmu-default-outlet" }) => {
|
|
214
|
-
const ctx = (0, react.useContext)(RMUContext);
|
|
215
|
-
if (!ctx) throw new Error("RMUProvider not found in component tree");
|
|
216
|
-
const { outlets, addOutlet, removeOutlet } = ctx;
|
|
217
|
-
(0, react.useEffect)(() => {
|
|
218
|
-
addOutlet(outletId);
|
|
219
|
-
return () => {
|
|
220
|
-
removeOutlet(outletId);
|
|
221
|
-
};
|
|
222
|
-
}, []);
|
|
223
|
-
const modals = outlets[outletId] ?? {};
|
|
224
|
-
return /* @__PURE__ */ react.default.createElement(react.default.Fragment, null, Object.entries(modals).map(([modalId, modalComponent]) => /* @__PURE__ */ react.default.createElement(react.Fragment, { key: modalId }, modalComponent)));
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
//#endregion
|
|
228
|
-
exports.RMUOutlet = RMUOutlet;
|
|
229
|
-
exports.RMUProvider = RMUProvider;
|
|
230
|
-
exports.closeModal = closeModal;
|
|
231
|
-
exports.openModal = openModal;
|