@codeleap/portals 6.3.0 → 7.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/dist/factors/alert.d.ts +44 -0
- package/dist/factors/alert.d.ts.map +1 -0
- package/dist/factors/bottomSheet.d.ts +61 -0
- package/dist/factors/bottomSheet.d.ts.map +1 -0
- package/dist/factors/drawer.d.ts +32 -0
- package/dist/factors/drawer.d.ts.map +1 -0
- package/dist/factors/modal.d.ts +32 -0
- package/dist/factors/modal.d.ts.map +1 -0
- package/dist/globals.d.ts +19 -0
- package/dist/globals.d.ts.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/lib/Portal.d.ts +121 -0
- package/dist/lib/Portal.d.ts.map +1 -0
- package/dist/lib/PortalRegistry.d.ts +59 -0
- package/dist/lib/PortalRegistry.d.ts.map +1 -0
- package/dist/lib/PortalRequest.d.ts +44 -0
- package/dist/lib/PortalRequest.d.ts.map +1 -0
- package/dist/lib/PortalState.d.ts +83 -0
- package/dist/lib/PortalState.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/misc.d.ts +7 -0
- package/dist/types/misc.d.ts.map +1 -0
- package/dist/types/portal.d.ts +40 -0
- package/dist/types/portal.d.ts.map +1 -0
- package/dist/utils.d.ts +12 -0
- package/dist/utils.d.ts.map +1 -0
- package/package.json +25 -11
- package/src/factors/alert.ts +20 -60
- package/src/factors/bottomSheet.ts +20 -3
- package/src/factors/drawer.ts +11 -1
- package/src/factors/modal.ts +11 -1
- package/src/globals.ts +6 -0
- package/src/lib/Portal.tsx +62 -14
- package/src/lib/PortalRequest.ts +5 -5
- package/src/lib/PortalState.ts +64 -25
- package/src/types/misc.ts +2 -0
- package/src/types/portal.ts +11 -5
- package/src/utils.ts +9 -0
- package/package.json.bak +0 -32
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates a random 7-character alphanumeric ID.
|
|
3
|
+
* @returns Random ID string
|
|
4
|
+
*/
|
|
5
|
+
export declare const randomId: () => string;
|
|
6
|
+
/**
|
|
7
|
+
* Waits for one or more animation transitions to complete.
|
|
8
|
+
* @param count - Number of transitions to wait for (defaults to 1)
|
|
9
|
+
* @param duration - Duration per transition in milliseconds (defaults to 1000)
|
|
10
|
+
*/
|
|
11
|
+
export declare function awaitTransition(count?: number, duration?: number): Promise<void>;
|
|
12
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,eAAO,MAAM,QAAQ,cAEpB,CAAA;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,QAAQ,SAAO,iBAIpE"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codeleap/portals",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"main": "src/index.ts",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"source": "./src/index.ts",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src"
|
|
17
|
+
],
|
|
5
18
|
"license": "UNLICENSED",
|
|
6
19
|
"repository": {
|
|
7
20
|
"url": "https://github.com/codeleap-uk/internal-libs-monorepo.git",
|
|
@@ -9,24 +22,25 @@
|
|
|
9
22
|
"directory": "packages/portals"
|
|
10
23
|
},
|
|
11
24
|
"devDependencies": {
|
|
12
|
-
"@codeleap/types": "
|
|
13
|
-
"@codeleap/config": "
|
|
14
|
-
"@codeleap/utils": "
|
|
15
|
-
"@codeleap/logger": "
|
|
25
|
+
"@codeleap/types": "7.0.0",
|
|
26
|
+
"@codeleap/config": "7.0.0",
|
|
27
|
+
"@codeleap/utils": "7.0.0",
|
|
28
|
+
"@codeleap/logger": "7.0.0",
|
|
16
29
|
"ts-node-dev": "1.1.8"
|
|
17
30
|
},
|
|
18
31
|
"scripts": {
|
|
19
|
-
"build": "
|
|
32
|
+
"build": "tsc --build tsconfig.build.json",
|
|
33
|
+
"typecheck": "bun tsc --noEmit -p ./tsconfig.json",
|
|
20
34
|
"lint": "eslint -c .eslintrc.js --fix \"./src/**/*.{ts,tsx,js,jsx}\"",
|
|
21
35
|
"run-sc": "tsnd --transpile-only"
|
|
22
36
|
},
|
|
23
37
|
"peerDependencies": {
|
|
24
|
-
"@codeleap/types": "
|
|
25
|
-
"@codeleap/utils": "
|
|
26
|
-
"@codeleap/logger": "
|
|
27
|
-
"typescript": "
|
|
38
|
+
"@codeleap/types": "7.0.0",
|
|
39
|
+
"@codeleap/utils": "7.0.0",
|
|
40
|
+
"@codeleap/logger": "7.0.0",
|
|
41
|
+
"typescript": "6.0.3",
|
|
28
42
|
"react": "19.1.0",
|
|
29
43
|
"nanostores": "*",
|
|
30
44
|
"@nanostores/react": "*"
|
|
31
45
|
}
|
|
32
|
-
}
|
|
46
|
+
}
|
package/src/factors/alert.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { AnyFunction } from '@codeleap/types'
|
|
2
2
|
import { Modal } from './modal'
|
|
3
3
|
|
|
4
|
+
/** Discriminant for alert categories used to control icon/style in the wrapper component. */
|
|
4
5
|
export type AlertType = 'info' | 'error' | 'warn' | 'ask' | 'custom'
|
|
5
6
|
|
|
7
|
+
/** A single button option rendered inside an alert dialog. */
|
|
6
8
|
export type AlertOption = {
|
|
7
9
|
text: string
|
|
8
10
|
onPress?: AnyFunction
|
|
9
11
|
style?: 'destructive' | 'outline' | 'cancel' | 'default' | 'minimal'
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
/** Configuration options for displaying an alert dialog. */
|
|
12
15
|
export type AlertOptions = {
|
|
13
16
|
title?: string
|
|
14
17
|
body?: string
|
|
@@ -22,88 +25,45 @@ export type AlertOptions = {
|
|
|
22
25
|
* Provides convenient methods for common alert scenarios.
|
|
23
26
|
*/
|
|
24
27
|
export class Alert {
|
|
25
|
-
static modal: Modal<AlertOptions>
|
|
26
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Returns the Modal instance used to display alerts.
|
|
31
|
+
* Must be implemented by the consuming app before calling any alert method.
|
|
32
|
+
* @throws Error if not implemented
|
|
33
|
+
*/
|
|
27
34
|
static openAlert(): Modal<AlertOptions> {
|
|
28
|
-
|
|
35
|
+
throw new Error('Please implement Alert.openAlert to use the alert system')
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
private trigger(args: AlertOptions) {
|
|
32
39
|
Alert.openAlert().open(args)
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
/**
|
|
36
|
-
* Opens an alert with 'ask' type (for questions).
|
|
37
|
-
* @param args - Alert options (defaults title to 'Quick question')
|
|
38
|
-
*/
|
|
42
|
+
/** Opens an alert with 'ask' type. */
|
|
39
43
|
ask(args: AlertOptions) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
this.trigger({
|
|
43
|
-
...rest,
|
|
44
|
-
title,
|
|
45
|
-
type: 'ask',
|
|
46
|
-
})
|
|
44
|
+
this.trigger({ ...args, type: 'ask' })
|
|
47
45
|
}
|
|
48
46
|
|
|
49
|
-
/**
|
|
50
|
-
* Opens an alert with 'error' type.
|
|
51
|
-
* @param args - Alert options (defaults title to 'Whoops!' and body to 'Something went wrong')
|
|
52
|
-
*/
|
|
47
|
+
/** Opens an alert with 'error' type. */
|
|
53
48
|
error(args: AlertOptions) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
this.trigger({
|
|
57
|
-
...rest,
|
|
58
|
-
title,
|
|
59
|
-
body,
|
|
60
|
-
type: 'error',
|
|
61
|
-
})
|
|
49
|
+
this.trigger({ ...args, type: 'error' })
|
|
62
50
|
}
|
|
63
51
|
|
|
64
|
-
/**
|
|
65
|
-
* Opens an alert with 'warn' type.
|
|
66
|
-
* @param args - Alert options (defaults title to 'Hang on' and body to 'Are you sure?')
|
|
67
|
-
*/
|
|
52
|
+
/** Opens an alert with 'warn' type. */
|
|
68
53
|
warn(args: AlertOptions) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
this.trigger({
|
|
72
|
-
...rest,
|
|
73
|
-
title,
|
|
74
|
-
body,
|
|
75
|
-
type: 'warn',
|
|
76
|
-
})
|
|
54
|
+
this.trigger({ ...args, type: 'warn' })
|
|
77
55
|
}
|
|
78
56
|
|
|
79
|
-
/**
|
|
80
|
-
* Opens an alert with 'info' type.
|
|
81
|
-
* @param args - Alert options (defaults title to 'Hang on' and body to 'Are you sure?')
|
|
82
|
-
*/
|
|
57
|
+
/** Opens an alert with 'info' type. */
|
|
83
58
|
info(args: AlertOptions) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
this.trigger({
|
|
87
|
-
...rest,
|
|
88
|
-
title,
|
|
89
|
-
body,
|
|
90
|
-
type: 'info',
|
|
91
|
-
})
|
|
59
|
+
this.trigger({ ...args, type: 'info' })
|
|
92
60
|
}
|
|
93
61
|
|
|
94
|
-
/**
|
|
95
|
-
* Opens a custom alert with user-defined options and type.
|
|
96
|
-
* @param options - Custom alert options and additional properties
|
|
97
|
-
*/
|
|
62
|
+
/** Opens a custom alert with user-defined options and type. */
|
|
98
63
|
custom<T>(options: Partial<AlertOptions> & T) {
|
|
99
|
-
this.trigger({
|
|
100
|
-
...(options as any),
|
|
101
|
-
type: 'custom',
|
|
102
|
-
})
|
|
64
|
+
this.trigger({ ...(options as any), type: 'custom' })
|
|
103
65
|
}
|
|
104
66
|
}
|
|
105
67
|
|
|
106
|
-
/**
|
|
107
|
-
* Global alert instance for showing alerts throughout the app.
|
|
108
|
-
*/
|
|
68
|
+
/** Global alert instance for showing alerts throughout the app. */
|
|
109
69
|
export const alert = new Alert()
|
|
@@ -15,22 +15,35 @@ import { ReactElement } from 'react'
|
|
|
15
15
|
export class BottomSheet<Params = {}, Result = {}, Metadata = {}> extends Portal<Params, Result, Metadata, BottomSheetRef, BottomSheetWrapperProps> {
|
|
16
16
|
displayName = 'BottomSheet'
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
/** Wrapper component used to render bottom sheet UI. Must be assigned by the consuming app. */
|
|
19
|
+
static WrapperComponent: (props: PortalWrapperProps<BottomSheetWrapperProps, BottomSheetRef>) => ReactElement | null = () => null
|
|
19
20
|
|
|
21
|
+
/** Default transition duration in milliseconds for bottom sheet open/close animations. */
|
|
20
22
|
static DEFAULT_TRANSITION_DURATION = 200
|
|
21
23
|
|
|
24
|
+
/** @inheritdoc */
|
|
22
25
|
protected get defaultTransitionDuration() {
|
|
23
26
|
return BottomSheet.DEFAULT_TRANSITION_DURATION
|
|
24
27
|
}
|
|
25
28
|
|
|
29
|
+
/** @inheritdoc */
|
|
26
30
|
protected get wrapperComponent(): any {
|
|
27
31
|
return BottomSheet.WrapperComponent
|
|
28
32
|
}
|
|
29
33
|
|
|
34
|
+
/** Closes all open BottomSheet instances. */
|
|
35
|
+
static async closeAll() {
|
|
36
|
+
const registries = BottomSheet.registry.getAll()
|
|
37
|
+
await Promise.all(registries.map(instance => instance.close()))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Name of the ref method to call imperatively when opening the sheet (set by the consuming app). */
|
|
30
41
|
static openKeyMethod: Keyof<BottomSheetRef>
|
|
31
42
|
|
|
43
|
+
/** Name of the ref method to call imperatively when closing the sheet (set by the consuming app). */
|
|
32
44
|
static closeKeyMethod: Keyof<BottomSheetRef>
|
|
33
45
|
|
|
46
|
+
/** Returns default config: `rendersWhenHidden=true` and `resetParamsOnClose=false`. */
|
|
34
47
|
protected getDefaultConfig(): Partial<PortalConfig<Params, Metadata>> {
|
|
35
48
|
return {
|
|
36
49
|
rendersWhenHidden: true,
|
|
@@ -38,22 +51,26 @@ export class BottomSheet<Params = {}, Result = {}, Metadata = {}> extends Portal
|
|
|
38
51
|
} as Partial<PortalConfig<Params, Metadata>>
|
|
39
52
|
}
|
|
40
53
|
|
|
54
|
+
/** Calls the native open method on the ref via `openKeyMethod`, if configured. */
|
|
41
55
|
protected openBottomSheet() {
|
|
42
56
|
if (!!BottomSheet.openKeyMethod && !!this.ref?.current) {
|
|
43
|
-
this.ref?.current?.[BottomSheet.openKeyMethod as string]?.()
|
|
57
|
+
(this.ref?.current as Record<string, (() => void) | undefined>)?.[BottomSheet.openKeyMethod as string]?.()
|
|
44
58
|
}
|
|
45
59
|
}
|
|
46
60
|
|
|
61
|
+
/** Calls the native close method on the ref via `closeKeyMethod`, if configured. */
|
|
47
62
|
protected closeBottomSheet() {
|
|
48
63
|
if (!!BottomSheet.closeKeyMethod && !!this.ref?.current) {
|
|
49
|
-
this.ref?.current?.[BottomSheet.closeKeyMethod as string]?.()
|
|
64
|
+
(this.ref?.current as Record<string, (() => void) | undefined>)?.[BottomSheet.closeKeyMethod as string]?.()
|
|
50
65
|
}
|
|
51
66
|
}
|
|
52
67
|
|
|
68
|
+
/** @inheritdoc — also triggers the native open animation via `openBottomSheet()`. */
|
|
53
69
|
protected handleOpen() {
|
|
54
70
|
this.openBottomSheet()
|
|
55
71
|
}
|
|
56
72
|
|
|
73
|
+
/** @inheritdoc — also triggers the native close animation via `closeBottomSheet()`. */
|
|
57
74
|
protected handleClose() {
|
|
58
75
|
this.closeBottomSheet()
|
|
59
76
|
}
|
package/src/factors/drawer.ts
CHANGED
|
@@ -14,17 +14,27 @@ import { PortalConstructorParam, PortalWrapperProps } from '../types'
|
|
|
14
14
|
export class Drawer<Params = {}, Result = {}, Metadata = {}> extends Portal<Params, Result, Metadata, DrawerRef, DrawerWrapperProps> {
|
|
15
15
|
displayName = 'Drawer'
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
/** Wrapper component used to render drawer UI. Must be assigned by the consuming app. */
|
|
18
|
+
static WrapperComponent: (props: PortalWrapperProps<DrawerWrapperProps, DrawerRef>) => ReactElement | null = () => null
|
|
18
19
|
|
|
20
|
+
/** Default transition duration in milliseconds for drawer open/close animations. */
|
|
19
21
|
static DEFAULT_TRANSITION_DURATION = 200
|
|
20
22
|
|
|
23
|
+
/** @inheritdoc */
|
|
21
24
|
protected get defaultTransitionDuration() {
|
|
22
25
|
return Drawer.DEFAULT_TRANSITION_DURATION
|
|
23
26
|
}
|
|
24
27
|
|
|
28
|
+
/** @inheritdoc */
|
|
25
29
|
protected get wrapperComponent(): any {
|
|
26
30
|
return Drawer.WrapperComponent
|
|
27
31
|
}
|
|
32
|
+
|
|
33
|
+
/** Closes all open Drawer instances. */
|
|
34
|
+
static async closeAll() {
|
|
35
|
+
const registries = Drawer.registry.getAll()
|
|
36
|
+
await Promise.all(registries.map(instance => instance.close()))
|
|
37
|
+
}
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
/**
|
package/src/factors/modal.ts
CHANGED
|
@@ -14,17 +14,27 @@ import { PortalConstructorParam, PortalWrapperProps } from '../types'
|
|
|
14
14
|
export class Modal<Params = {}, Result = {}, Metadata = {}> extends Portal<Params, Result, Metadata, ModalRef, ModalWrapperProps> {
|
|
15
15
|
displayName = 'Modal'
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
/** Wrapper component used to render modal UI. Must be assigned by the consuming app. */
|
|
18
|
+
static WrapperComponent: (props: PortalWrapperProps<ModalWrapperProps, ModalRef>) => ReactElement | null = () => null
|
|
18
19
|
|
|
20
|
+
/** Default transition duration in milliseconds for modal open/close animations. */
|
|
19
21
|
static DEFAULT_TRANSITION_DURATION = 200
|
|
20
22
|
|
|
23
|
+
/** @inheritdoc */
|
|
21
24
|
protected get defaultTransitionDuration() {
|
|
22
25
|
return Modal.DEFAULT_TRANSITION_DURATION
|
|
23
26
|
}
|
|
24
27
|
|
|
28
|
+
/** @inheritdoc */
|
|
25
29
|
protected get wrapperComponent(): any {
|
|
26
30
|
return Modal.WrapperComponent
|
|
27
31
|
}
|
|
32
|
+
|
|
33
|
+
/** Closes all open Modal instances. */
|
|
34
|
+
static async closeAll() {
|
|
35
|
+
const registries = Modal.registry.getAll()
|
|
36
|
+
await Promise.all(registries.map(instance => instance.close()))
|
|
37
|
+
}
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
/**
|
package/src/globals.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-empty-interface */
|
|
2
2
|
|
|
3
|
+
/** Props for the bottom sheet wrapper component. Extend this interface in the consuming app. */
|
|
3
4
|
export interface BottomSheetWrapperProps {}
|
|
4
5
|
|
|
6
|
+
/** Ref type for the bottom sheet component. Extend this interface to expose imperative methods. */
|
|
5
7
|
export interface BottomSheetRef {}
|
|
6
8
|
|
|
9
|
+
/** Props for the modal wrapper component. Extend this interface in the consuming app. */
|
|
7
10
|
export interface ModalWrapperProps {}
|
|
8
11
|
|
|
12
|
+
/** Ref type for the modal component. Extend this interface to expose imperative methods. */
|
|
9
13
|
export interface ModalRef {}
|
|
10
14
|
|
|
15
|
+
/** Props for the drawer wrapper component. Extend this interface in the consuming app. */
|
|
11
16
|
export interface DrawerWrapperProps {}
|
|
12
17
|
|
|
18
|
+
/** Ref type for the drawer component. Extend this interface to expose imperative methods. */
|
|
13
19
|
export interface DrawerRef {}
|
package/src/lib/Portal.tsx
CHANGED
|
@@ -11,6 +11,11 @@ import { PortalRequest } from './PortalRequest'
|
|
|
11
11
|
|
|
12
12
|
type IAtom<T> = ReturnType<typeof atom<T>>
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Nanostores atom holding the IDs of all currently registered portals in creation order.
|
|
16
|
+
* Drives `GlobalOutlet` re-renders when portals are added or removed (including ephemeral ones).
|
|
17
|
+
* Read-only outside of portal lifecycle code — mutate only via `Portal` constructor/`onVisibilityChanged`.
|
|
18
|
+
*/
|
|
14
19
|
export const registeredIds = atom<string[]>([])
|
|
15
20
|
|
|
16
21
|
/**
|
|
@@ -28,9 +33,9 @@ export class Portal<Params = {}, Result = {}, Metadata = {}, RefType = any, Wrap
|
|
|
28
33
|
|
|
29
34
|
id: string
|
|
30
35
|
|
|
31
|
-
RenderContent
|
|
36
|
+
RenderContent!: RenderPortalContent<Params, Result, RefType>
|
|
32
37
|
|
|
33
|
-
ref?: React.RefObject<RefType>
|
|
38
|
+
ref?: React.RefObject<RefType | null>
|
|
34
39
|
|
|
35
40
|
requestHandler: PortalRequest<Params, Result>
|
|
36
41
|
|
|
@@ -38,7 +43,7 @@ export class Portal<Params = {}, Result = {}, Metadata = {}, RefType = any, Wrap
|
|
|
38
43
|
|
|
39
44
|
_wrapperProps: IAtom<WrapperProps>
|
|
40
45
|
|
|
41
|
-
_lazyWrapperProps
|
|
46
|
+
_lazyWrapperProps!: (params: Params) => WrapperProps
|
|
42
47
|
|
|
43
48
|
static registry = new PortalRegistry<Portal<any, any, any>>()
|
|
44
49
|
|
|
@@ -56,7 +61,7 @@ export class Portal<Params = {}, Result = {}, Metadata = {}, RefType = any, Wrap
|
|
|
56
61
|
return Portal.generateId
|
|
57
62
|
}
|
|
58
63
|
|
|
59
|
-
protected get defaultTransitionDuration() {
|
|
64
|
+
protected get defaultTransitionDuration(): number | null {
|
|
60
65
|
return null
|
|
61
66
|
}
|
|
62
67
|
|
|
@@ -74,15 +79,17 @@ export class Portal<Params = {}, Result = {}, Metadata = {}, RefType = any, Wrap
|
|
|
74
79
|
return this
|
|
75
80
|
}
|
|
76
81
|
|
|
82
|
+
/** Gets the resolve handler for the pending request. */
|
|
77
83
|
get resolve() {
|
|
78
84
|
return this.requestHandler.resolve
|
|
79
85
|
}
|
|
80
86
|
|
|
87
|
+
/** Gets the reject handler for the pending request. */
|
|
81
88
|
get reject() {
|
|
82
89
|
return this.requestHandler.reject
|
|
83
90
|
}
|
|
84
91
|
|
|
85
|
-
private onVisibilityChanged(visible: boolean, wasVisible
|
|
92
|
+
private onVisibilityChanged(visible: boolean, wasVisible?: boolean) {
|
|
86
93
|
if (this._config.independent) {
|
|
87
94
|
return
|
|
88
95
|
}
|
|
@@ -92,19 +99,30 @@ export class Portal<Params = {}, Result = {}, Metadata = {}, RefType = any, Wrap
|
|
|
92
99
|
} else {
|
|
93
100
|
if (wasVisible && !visible) {
|
|
94
101
|
if (this.hasPendingRequest) {
|
|
95
|
-
this.requestHandler.resolve?.(
|
|
102
|
+
this.requestHandler.resolve?.(undefined)
|
|
96
103
|
this.requestHandler.clearRequest()
|
|
97
104
|
}
|
|
105
|
+
|
|
106
|
+
if (this._config.ephemeral) {
|
|
107
|
+
this.registry.unregister(this.id)
|
|
108
|
+
registeredIds.set(registeredIds.get().filter(id => id !== this.id))
|
|
109
|
+
return
|
|
110
|
+
}
|
|
98
111
|
}
|
|
99
112
|
|
|
100
113
|
this.registry.remove(this.id)
|
|
101
114
|
}
|
|
102
115
|
}
|
|
103
116
|
|
|
117
|
+
/** Gets the z-index position of the portal in the registry stack. */
|
|
104
118
|
get stackIndex() {
|
|
105
119
|
return this.registry.getStackIndex(this.id)
|
|
106
120
|
}
|
|
107
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Creates a new portal instance.
|
|
124
|
+
* @param idOrConfig - Portal ID string or configuration object with optional id field
|
|
125
|
+
*/
|
|
108
126
|
constructor(idOrConfig?: PortalConstructorParam<Params, Metadata>) {
|
|
109
127
|
super()
|
|
110
128
|
|
|
@@ -124,10 +142,10 @@ export class Portal<Params = {}, Result = {}, Metadata = {}, RefType = any, Wrap
|
|
|
124
142
|
}
|
|
125
143
|
|
|
126
144
|
this.initializeState({
|
|
127
|
-
initialParams: this._config.initialParams
|
|
128
|
-
startsOpen: this._config.startsOpen
|
|
145
|
+
initialParams: this._config.initialParams!,
|
|
146
|
+
startsOpen: this._config.startsOpen!,
|
|
129
147
|
resetParamsOnClose: this._config.resetParamsOnClose,
|
|
130
|
-
transitionDuration: this._config.transitionDuration
|
|
148
|
+
transitionDuration: this._config.transitionDuration!,
|
|
131
149
|
})
|
|
132
150
|
|
|
133
151
|
this.id = id ?? this.idGenerator()
|
|
@@ -157,6 +175,7 @@ export class Portal<Params = {}, Result = {}, Metadata = {}, RefType = any, Wrap
|
|
|
157
175
|
this.resetParams = this.resetParams.bind(this)
|
|
158
176
|
this.getParams = this.getParams.bind(this)
|
|
159
177
|
this.request = this.request.bind(this)
|
|
178
|
+
this.assertInitialized = this.assertInitialized.bind(this)
|
|
160
179
|
}
|
|
161
180
|
|
|
162
181
|
/**
|
|
@@ -213,12 +232,16 @@ export class Portal<Params = {}, Result = {}, Metadata = {}, RefType = any, Wrap
|
|
|
213
232
|
* @returns Object with visible and params state
|
|
214
233
|
*/
|
|
215
234
|
useState() {
|
|
216
|
-
|
|
217
|
-
const params =
|
|
235
|
+
|
|
236
|
+
const { visible: visibleStore, params: paramsStore } = this.assertInitialized()
|
|
237
|
+
|
|
238
|
+
const visible = useStore(visibleStore)
|
|
239
|
+
const params = useStore(paramsStore)
|
|
218
240
|
|
|
219
241
|
return { visible, params }
|
|
220
242
|
}
|
|
221
243
|
|
|
244
|
+
/** Whether there is a pending request awaiting resolution. */
|
|
222
245
|
get hasPendingRequest() {
|
|
223
246
|
return this.requestHandler.hasPendingRequest
|
|
224
247
|
}
|
|
@@ -228,11 +251,22 @@ export class Portal<Params = {}, Result = {}, Metadata = {}, RefType = any, Wrap
|
|
|
228
251
|
* @param props - Combined params and wrapper props
|
|
229
252
|
* @returns JSX element with portal content wrapped in wrapper component
|
|
230
253
|
*/
|
|
231
|
-
Component(props?: Params & { portalProps?: WrapperProps }): JSX.Element {
|
|
254
|
+
Component(props?: Params & { portalProps?: WrapperProps }): JSX.Element | null {
|
|
232
255
|
const { visible, params } = this.useState()
|
|
233
256
|
const wrapperProps = useStore(this._wrapperProps)
|
|
234
257
|
const { portalProps = {}, ...propParams } = props || {}
|
|
235
258
|
|
|
259
|
+
const [mounted, setMounted] = React.useState(visible)
|
|
260
|
+
|
|
261
|
+
React.useEffect(() => {
|
|
262
|
+
if (visible) {
|
|
263
|
+
setMounted(true)
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
const timer = setTimeout(() => setMounted(false), this._config?.transitionDuration ?? 0)
|
|
267
|
+
return () => clearTimeout(timer)
|
|
268
|
+
}, [visible])
|
|
269
|
+
|
|
236
270
|
const Content = this.RenderContent
|
|
237
271
|
|
|
238
272
|
if (!Content) {
|
|
@@ -240,7 +274,7 @@ export class Portal<Params = {}, Result = {}, Metadata = {}, RefType = any, Wrap
|
|
|
240
274
|
return null
|
|
241
275
|
}
|
|
242
276
|
|
|
243
|
-
if (!visible && !this._config?.rendersWhenHidden) return null
|
|
277
|
+
if (!visible && !mounted && !this._config?.rendersWhenHidden) return null
|
|
244
278
|
|
|
245
279
|
const request = this.requestHandler.getRequestHandlers()
|
|
246
280
|
|
|
@@ -261,6 +295,7 @@ export class Portal<Params = {}, Result = {}, Metadata = {}, RefType = any, Wrap
|
|
|
261
295
|
zIndex={this.stackIndex}
|
|
262
296
|
ref={this.ref}
|
|
263
297
|
>
|
|
298
|
+
{/* @ts-ignore — Content is a call signature, not a component type; LibraryManagedAttributes can't verify the spread */}
|
|
264
299
|
<Content
|
|
265
300
|
visible={visible}
|
|
266
301
|
toggle={this.toggle}
|
|
@@ -269,7 +304,7 @@ export class Portal<Params = {}, Result = {}, Metadata = {}, RefType = any, Wrap
|
|
|
269
304
|
setParams={this.setParams}
|
|
270
305
|
{...params}
|
|
271
306
|
{...(propParams ?? {})}
|
|
272
|
-
request={this.hasPendingRequest ? request :
|
|
307
|
+
request={this.hasPendingRequest ? request : undefined}
|
|
273
308
|
nextOrToggle={this.toggle}
|
|
274
309
|
previousOrToggle={this.toggle}
|
|
275
310
|
ref={this.ref}
|
|
@@ -287,6 +322,19 @@ export class Portal<Params = {}, Result = {}, Metadata = {}, RefType = any, Wrap
|
|
|
287
322
|
return this.requestHandler.request(params, force)
|
|
288
323
|
}
|
|
289
324
|
|
|
325
|
+
/**
|
|
326
|
+
* Resets all registered portals to their closed initial state.
|
|
327
|
+
* Useful in test afterEach hooks to prevent state leaking between tests.
|
|
328
|
+
*/
|
|
329
|
+
static resetAll() {
|
|
330
|
+
Portal.registry.getAll().forEach(portal => {
|
|
331
|
+
portal.visible?.set(false)
|
|
332
|
+
if (portal._initialParams !== undefined) {
|
|
333
|
+
portal.params?.set(portal._initialParams as any)
|
|
334
|
+
}
|
|
335
|
+
})
|
|
336
|
+
}
|
|
337
|
+
|
|
290
338
|
/**
|
|
291
339
|
* Global outlet component that renders all non-independent portals.
|
|
292
340
|
* Place this once in your app root.
|
package/src/lib/PortalRequest.ts
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* @template Result - Type of result returned when request resolves
|
|
7
7
|
*/
|
|
8
8
|
export class PortalRequest<Params = {}, Result = {}> {
|
|
9
|
-
resolve
|
|
9
|
+
resolve?: (result: Result | undefined) => void
|
|
10
10
|
|
|
11
|
-
reject
|
|
11
|
+
reject?: (reason: unknown) => void
|
|
12
12
|
|
|
13
13
|
private onOpen: (params?: Params) => Promise<void>
|
|
14
14
|
|
|
@@ -41,13 +41,13 @@ export class PortalRequest<Params = {}, Result = {}> {
|
|
|
41
41
|
* @param force - Force new request even if one is pending
|
|
42
42
|
* @returns Promise that resolves with portal result
|
|
43
43
|
*/
|
|
44
|
-
request(params?: Params, force = false): Promise<Result> {
|
|
44
|
+
request(params?: Params, force = false): Promise<Result|undefined> {
|
|
45
45
|
if (this.hasPendingRequest && !force) {
|
|
46
46
|
return Promise.reject(new Error('This portal already has a pending request'))
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
return new Promise<Result>((resolve, reject) => {
|
|
50
|
-
const onResolve = (result: Result) => {
|
|
49
|
+
return new Promise<Result|undefined>((resolve, reject) => {
|
|
50
|
+
const onResolve = (result: Result|undefined) => {
|
|
51
51
|
resolve(result)
|
|
52
52
|
this.onClose()
|
|
53
53
|
this.clearRequest()
|