@linzjs/windows 1.5.2 → 2.0.1
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/LuiModalAsync/LuiModalAsync.scss +100 -0
- package/dist/LuiModalAsync/LuiModalAsync.tsx +75 -0
- package/dist/LuiModalAsync/LuiModalAsyncButtonGroup.tsx +55 -0
- package/dist/LuiModalAsync/LuiModalAsyncContent.tsx +9 -0
- package/dist/LuiModalAsync/LuiModalAsyncContext.tsx +37 -0
- package/dist/{modal/ModalContextProvider.tsx → LuiModalAsync/LuiModalAsyncContextProvider.tsx} +50 -43
- package/dist/LuiModalAsync/LuiModalAsyncHeader.tsx +58 -0
- package/dist/LuiModalAsync/LuiModalAsyncInstanceContext.ts +23 -0
- package/dist/LuiModalAsync/LuiModalAsyncMain.tsx +8 -0
- package/dist/LuiModalAsync/index.ts +10 -0
- package/dist/LuiModalAsync/useLuiModalPrefab.tsx +202 -0
- package/dist/LuiModalAsync/useShowAsyncModal.ts +24 -0
- package/dist/index.ts +1 -1
- package/dist/panel/PanelsContextProvider.tsx +1 -1
- package/package.json +48 -47
- package/dist/modal/Modal.tsx +0 -45
- package/dist/modal/ModalContext.scss +0 -11
- package/dist/modal/ModalContext.tsx +0 -27
- package/dist/modal/ModalInstanceContext.ts +0 -16
- package/dist/modal/PrefabModal.scss +0 -21
- package/dist/modal/PrefabModal.tsx +0 -106
- package/dist/modal/index.ts +0 -6
- package/dist/modal/useShowModal.ts +0 -16
- package/dist/util/useInterval.ts +0 -18
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
@use "node_modules/@linzjs/lui/dist/scss/Core" as lui;
|
|
2
|
+
|
|
3
|
+
dialog.LuiModalAsync::backdrop {
|
|
4
|
+
background: rgb(0 0 0 / 10%);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
dialog.LuiModalAsync, div.LuiModalAsync {
|
|
8
|
+
@include lui.drop-shadow(sm);
|
|
9
|
+
border: none;
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
border-radius: lui.$unit-xs;
|
|
13
|
+
padding: lui.$unit-md;
|
|
14
|
+
min-width: 400px;
|
|
15
|
+
max-width: 80vw;
|
|
16
|
+
max-height: 80vh;
|
|
17
|
+
gap: lui.$unit-md;
|
|
18
|
+
|
|
19
|
+
&.LuiModalPrefab-success {
|
|
20
|
+
border-left: lui.$unit-xs solid lui.$success;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
&.LuiModalPrefab-info {
|
|
24
|
+
border-left: lui.$unit-xs solid lui.$info;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
&.LuiModalPrefab-warning {
|
|
28
|
+
border-left: lui.$unit-xs solid lui.$warning;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
&.LuiModalPrefab-error {
|
|
32
|
+
border-left: lui.$unit-xs solid lui.$error;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
h4 {
|
|
36
|
+
margin-top: 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.LuiModalAsync-ButtonGroup {
|
|
40
|
+
display: flex;
|
|
41
|
+
|
|
42
|
+
button {
|
|
43
|
+
flex: 1;
|
|
44
|
+
border-color: transparent;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.LuiModalAsync-header {
|
|
49
|
+
display: flex;
|
|
50
|
+
gap: lui.$unit-xs;
|
|
51
|
+
|
|
52
|
+
flex-direction: row;
|
|
53
|
+
align-items: center;
|
|
54
|
+
|
|
55
|
+
h4 {
|
|
56
|
+
flex: 1;
|
|
57
|
+
color: lui.$charcoal;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.LuiModalAsync-main {
|
|
62
|
+
display: flex;
|
|
63
|
+
flex-direction: column;
|
|
64
|
+
flex: 1;
|
|
65
|
+
gap: lui.$unit-rg;
|
|
66
|
+
overflow: hidden;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.LuiModalAsync-content {
|
|
70
|
+
flex: 1;
|
|
71
|
+
overflow: auto;
|
|
72
|
+
font-weight: 400;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.lui-button:focus {
|
|
76
|
+
// Button focus not showing unless window content has focus
|
|
77
|
+
outline: lui.$input-border-width solid lui.$input-focus;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.LuiModalAsync-header-icon {
|
|
81
|
+
fill: lui.$info;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.LuiModalAsync-header-icon.LuiModalAsync-header-icon-ic_check_circle_outline {
|
|
85
|
+
fill: lui.$success;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.LuiModalAsync-header-icon.LuiModalAsync-header-icon-ic_warning_outline {
|
|
89
|
+
fill: lui.$warning;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.LuiModalAsync-header-icon.LuiModalAsync-header-icon-ic_info_outline {
|
|
93
|
+
fill: lui.$info;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.LuiModalAsync-header-icon.LuiModalAsync-header-icon-ic_error_outline {
|
|
97
|
+
fill: lui.$error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import "./LuiModalAsync.scss";
|
|
2
|
+
import "@linzjs/lui/dist/scss/base.scss";
|
|
3
|
+
|
|
4
|
+
import { LuiModalAsyncInstanceContext } from "./LuiModalAsyncInstanceContext";
|
|
5
|
+
import clsx from "clsx";
|
|
6
|
+
import { delay } from "lodash";
|
|
7
|
+
import React, { CSSProperties, PropsWithChildren, ReactElement, useContext, useEffect, useRef } from "react";
|
|
8
|
+
|
|
9
|
+
import "@linzjs/lui/dist/fonts";
|
|
10
|
+
|
|
11
|
+
export interface LuiModalAsyncProps {
|
|
12
|
+
className?: string;
|
|
13
|
+
style?: CSSProperties;
|
|
14
|
+
/**
|
|
15
|
+
* Close on clicking overlay
|
|
16
|
+
*/
|
|
17
|
+
closeOnOverlayClick?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* This creates the dialog element.
|
|
22
|
+
*/
|
|
23
|
+
export const LuiModalAsync = ({
|
|
24
|
+
style,
|
|
25
|
+
className,
|
|
26
|
+
closeOnOverlayClick,
|
|
27
|
+
children,
|
|
28
|
+
}: PropsWithChildren<LuiModalAsyncProps>): ReactElement => {
|
|
29
|
+
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
30
|
+
|
|
31
|
+
const { close } = useContext(LuiModalAsyncInstanceContext);
|
|
32
|
+
|
|
33
|
+
// The only way to create a modal dialog is to call showModal, open attribute will not work
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
// Check if it's open already to support .vite hot deploys
|
|
36
|
+
!dialogRef.current?.open && dialogRef.current?.showModal();
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
// Dialogs auto select the first focusable element, this is in case you don't want that
|
|
41
|
+
// For example we don't want to select the help button by default, so we add data-noautofocus
|
|
42
|
+
delay(() => {
|
|
43
|
+
const d = dialogRef.current;
|
|
44
|
+
if (!d) return;
|
|
45
|
+
let input = d.querySelectorAll("[data-autofocus]")[0] as any;
|
|
46
|
+
if (!input) {
|
|
47
|
+
input = d.querySelectorAll("button:not([data-noautofocus]),input:not([data-noautofocus])")[0] as any;
|
|
48
|
+
}
|
|
49
|
+
if (input) {
|
|
50
|
+
input.focus?.();
|
|
51
|
+
input.select?.();
|
|
52
|
+
}
|
|
53
|
+
}, 100);
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
// You cannot override dialogs position from absolute.
|
|
57
|
+
return className?.includes("storybook") ? (
|
|
58
|
+
<div className={clsx("LuiModalAsync", className)} style={style}>
|
|
59
|
+
{children}
|
|
60
|
+
</div>
|
|
61
|
+
) : (
|
|
62
|
+
<dialog
|
|
63
|
+
className={clsx("LuiModalAsync", className)}
|
|
64
|
+
style={style}
|
|
65
|
+
ref={dialogRef}
|
|
66
|
+
onCancel={(e) => {
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
close();
|
|
69
|
+
}}
|
|
70
|
+
onClick={(e) => closeOnOverlayClick && e.target === e.currentTarget && close()}
|
|
71
|
+
>
|
|
72
|
+
{children}
|
|
73
|
+
</dialog>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { LuiModalAsyncInstanceContext } from "./LuiModalAsyncInstanceContext";
|
|
2
|
+
import React, { PropsWithChildren, useContext } from "react";
|
|
3
|
+
|
|
4
|
+
import { LuiButton } from "@linzjs/lui";
|
|
5
|
+
import { LuiButtonProps } from "@linzjs/lui/dist/components/LuiButton/LuiButton";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Wrapper for buttons at the bottom of model.
|
|
9
|
+
*/
|
|
10
|
+
export const LuiModalAsyncButtonGroup = ({ children }: PropsWithChildren<unknown>) => (
|
|
11
|
+
<div className={"LuiModalAsync-ButtonGroup"}>{children}</div>
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Calls model close on click.
|
|
16
|
+
*/
|
|
17
|
+
export const LuiModalAsyncButtonDismiss = React.forwardRef<HTMLButtonElement, LuiButtonProps & { autofocus?: boolean }>(
|
|
18
|
+
function LuiModalAsyncButtonDismiss(props, ref) {
|
|
19
|
+
return (
|
|
20
|
+
<LuiButton
|
|
21
|
+
{...props}
|
|
22
|
+
{...(props.level ? {} : { level: "tertiary" })}
|
|
23
|
+
{...(props.autofocus ? { buttonProps: { "data-autofocus": true } } : {})}
|
|
24
|
+
ref={ref}
|
|
25
|
+
onClick={useContext(LuiModalAsyncInstanceContext).close}
|
|
26
|
+
>
|
|
27
|
+
{props.children}
|
|
28
|
+
</LuiButton>
|
|
29
|
+
);
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolves to value on click, or if onClick is specified calls onClick.
|
|
35
|
+
*/
|
|
36
|
+
export const LuiModalAsyncButtonContinue = React.forwardRef<
|
|
37
|
+
HTMLButtonElement,
|
|
38
|
+
LuiButtonProps & { value?: any; autofocus?: boolean }
|
|
39
|
+
>(function LuiModalAsyncButtonContinue(props, ref) {
|
|
40
|
+
const { resolve } = useContext(LuiModalAsyncInstanceContext);
|
|
41
|
+
return (
|
|
42
|
+
<LuiButton
|
|
43
|
+
{...props}
|
|
44
|
+
{...(props.level ? {} : { level: "primary" })}
|
|
45
|
+
{...(props.autofocus ? { buttonProps: { "data-autofocus": true } } : {})}
|
|
46
|
+
ref={ref}
|
|
47
|
+
onClick={async (e) => {
|
|
48
|
+
if (props.onClick) props.onClick(e);
|
|
49
|
+
else resolve(props.value ?? "continue");
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
{props.children}
|
|
53
|
+
</LuiButton>
|
|
54
|
+
);
|
|
55
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React, { PropsWithChildren } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wrapper for modal content.
|
|
5
|
+
* Content is the scrollable area.
|
|
6
|
+
*/
|
|
7
|
+
export const LuiModalAsyncContent = ({ children }: PropsWithChildren<unknown>) => (
|
|
8
|
+
<div className={"LuiModalAsync-content"}>{children}</div>
|
|
9
|
+
);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ComponentProps, MutableRefObject, ReactElement, createContext } from "react";
|
|
2
|
+
|
|
3
|
+
export type ComponentType = (props: any) => ReactElement<any, any>;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Promise extended to add the ability to externally resolve dialog.
|
|
7
|
+
*/
|
|
8
|
+
export interface PromiseWithResolve<RT> extends Promise<RT> {
|
|
9
|
+
resolve: (value: RT) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Properties passed to the modal component to allow resolving.
|
|
14
|
+
*/
|
|
15
|
+
export interface LuiModalAsyncCallback<R> {
|
|
16
|
+
resolve: (result: R | undefined) => void;
|
|
17
|
+
close: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface LuiModalAsyncContextType {
|
|
21
|
+
showModal: <
|
|
22
|
+
OR extends HTMLElement | null,
|
|
23
|
+
PROPS extends LuiModalAsyncCallback<any>,
|
|
24
|
+
CT extends (props: PROPS) => ReactElement<any, any>,
|
|
25
|
+
RT = Parameters<ComponentProps<CT>["resolve"]>[0],
|
|
26
|
+
>(
|
|
27
|
+
ownerRef: MutableRefObject<OR>,
|
|
28
|
+
component: CT,
|
|
29
|
+
args: Omit<ComponentProps<CT>, "resolve" | "close">,
|
|
30
|
+
) => PromiseWithResolve<RT>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const LuiModalAsyncContext = createContext<LuiModalAsyncContextType>({
|
|
34
|
+
showModal: (async () => {
|
|
35
|
+
console.error("Missing LuiModalAsyncContext Provider");
|
|
36
|
+
}) as unknown as LuiModalAsyncContextType["showModal"],
|
|
37
|
+
});
|
package/dist/{modal/ModalContextProvider.tsx → LuiModalAsync/LuiModalAsyncContextProvider.tsx}
RENAMED
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
import "./
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import { ComponentType, ModalContext } from "./ModalContext";
|
|
5
|
-
import { ModalInstanceContext } from "./ModalInstanceContext";
|
|
6
|
-
import React, { Fragment, ReactNode } from "react";
|
|
7
|
-
import { MutableRefObject, ReactElement, useCallback, useState } from "react";
|
|
1
|
+
import { ComponentType, LuiModalAsyncContext, PromiseWithResolve } from "./LuiModalAsyncContext";
|
|
2
|
+
import { LuiModalAsyncInstanceContext } from "./LuiModalAsyncInstanceContext";
|
|
3
|
+
import React, { MutableRefObject, PropsWithChildren, ReactElement, useCallback, useState } from "react";
|
|
8
4
|
import { createPortal } from "react-dom";
|
|
5
|
+
import { useInterval } from "usehooks-ts";
|
|
9
6
|
import { v4 as makeUuid } from "uuid";
|
|
10
7
|
|
|
11
|
-
export interface
|
|
8
|
+
export interface LuiModalAsyncInstance {
|
|
12
9
|
uuid: string;
|
|
13
|
-
|
|
10
|
+
ownerRef: MutableRefObject<HTMLElement | null>;
|
|
14
11
|
componentInstance: ReactElement;
|
|
15
12
|
resolve: (result: any) => void;
|
|
16
13
|
}
|
|
@@ -22,9 +19,9 @@ export interface ModalInstance {
|
|
|
22
19
|
* <ol>
|
|
23
20
|
* <li>Add this Provider somewhere in your standard providers
|
|
24
21
|
* <pre>
|
|
25
|
-
* <
|
|
22
|
+
* <LuiModalAsyncContextProvider>
|
|
26
23
|
* ...children
|
|
27
|
-
* </
|
|
24
|
+
* </LuiModalAsyncContextProvider>
|
|
28
25
|
* </pre>
|
|
29
26
|
* </li>
|
|
30
27
|
* <li>Add the modal return type to the element as below, and use the props resolve/close from ModalProps for results.
|
|
@@ -36,7 +33,7 @@ export interface ModalInstance {
|
|
|
36
33
|
* export const SomeModal = ({initialState, resolve, close}: SomeModalProps): ReactElement => {
|
|
37
34
|
* return Modal(
|
|
38
35
|
* <div>
|
|
39
|
-
* Itsa me, I'm modal
|
|
36
|
+
* Itsa me, I'm a modal
|
|
40
37
|
* <button onClick={close}>Cancel</button>
|
|
41
38
|
* <button onClick={()=>resolve(someProp)}>Save</button>
|
|
42
39
|
* </div>
|
|
@@ -59,28 +56,30 @@ export interface ModalInstance {
|
|
|
59
56
|
* </li>
|
|
60
57
|
* </ol>
|
|
61
58
|
*/
|
|
62
|
-
export const
|
|
63
|
-
const [modals, setModals] = useState<
|
|
59
|
+
export const LuiModalAsyncContextProvider = ({ children }: PropsWithChildren<unknown>): ReactElement => {
|
|
60
|
+
const [modals, setModals] = useState<LuiModalAsyncInstance[]>([]);
|
|
64
61
|
|
|
65
62
|
/**
|
|
66
63
|
* Inserts the modal into the page, and removes once modal has a result.
|
|
67
64
|
* Note: full generic types are provided by the interface. Function def has been simplified here.
|
|
68
65
|
*
|
|
69
|
-
* @param ownerRef Reference to
|
|
66
|
+
* @param ownerRef Reference to element that opened this dialog such that it works for popout windows.
|
|
70
67
|
* @param Component React component.
|
|
71
68
|
* @param args Arguments for react component.
|
|
72
69
|
*/
|
|
73
70
|
const showModal = useCallback(
|
|
74
|
-
|
|
71
|
+
(ownerRef: MutableRefObject<HTMLElement | null>, Component: ComponentType, args: any): PromiseWithResolve<any> => {
|
|
75
72
|
const uuid = makeUuid();
|
|
73
|
+
let extResolve: PromiseWithResolve<any>["resolve"];
|
|
76
74
|
const promise = new Promise((resolve) => {
|
|
75
|
+
extResolve = resolve;
|
|
77
76
|
try {
|
|
78
77
|
// If there are any exceptions the modal won't show
|
|
79
78
|
setModals((modals) => [
|
|
80
79
|
...modals,
|
|
81
80
|
{
|
|
82
81
|
uuid,
|
|
83
|
-
|
|
82
|
+
ownerRef,
|
|
84
83
|
componentInstance: <Component {...args} resolve={resolve} close={() => resolve(undefined)} />,
|
|
85
84
|
resolve,
|
|
86
85
|
},
|
|
@@ -89,49 +88,57 @@ export const ModalContextProvider = ({ children }: { children?: ReactNode | unde
|
|
|
89
88
|
console.error(e);
|
|
90
89
|
return;
|
|
91
90
|
}
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return result;
|
|
91
|
+
}).then((result) => {
|
|
92
|
+
// Close modal
|
|
93
|
+
setModals((modals) => modals.filter((e) => e.uuid !== uuid));
|
|
94
|
+
return result;
|
|
95
|
+
}) as PromiseWithResolve<any>;
|
|
96
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
97
|
+
promise.resolve = extResolve!;
|
|
98
|
+
return promise;
|
|
101
99
|
},
|
|
102
100
|
[],
|
|
103
101
|
);
|
|
104
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Check modal is still attached to a window.
|
|
105
|
+
*/
|
|
105
106
|
const modalHasView = useCallback(
|
|
106
|
-
(modalInstance:
|
|
107
|
+
(modalInstance: LuiModalAsyncInstance): boolean => !!modalInstance.ownerRef.current?.ownerDocument?.defaultView,
|
|
107
108
|
[],
|
|
108
109
|
);
|
|
109
110
|
|
|
110
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Tidy up modals that have closed because of an external window closing
|
|
113
|
+
*/
|
|
111
114
|
useInterval(() => {
|
|
112
115
|
const newModals = modals.filter(modalHasView);
|
|
113
116
|
newModals.length !== modals.length && setModals(newModals);
|
|
114
117
|
}, 500);
|
|
115
118
|
|
|
116
119
|
return (
|
|
117
|
-
<
|
|
120
|
+
<LuiModalAsyncContext.Provider
|
|
118
121
|
value={{
|
|
119
122
|
showModal,
|
|
120
123
|
}}
|
|
121
124
|
>
|
|
122
|
-
|
|
123
|
-
{modals
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
125
|
+
<>
|
|
126
|
+
{modals.filter(modalHasView).map((modalInstance) =>
|
|
127
|
+
createPortal(
|
|
128
|
+
<LuiModalAsyncInstanceContext.Provider
|
|
129
|
+
value={{
|
|
130
|
+
ownerRef: modalInstance.ownerRef,
|
|
131
|
+
close: () => modalInstance.resolve(undefined),
|
|
132
|
+
resolve: modalInstance.resolve,
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
{modalInstance.componentInstance}
|
|
136
|
+
</LuiModalAsyncInstanceContext.Provider>,
|
|
137
|
+
(modalInstance.ownerRef.current?.ownerDocument ?? document).body,
|
|
138
|
+
),
|
|
139
|
+
)}
|
|
140
|
+
</>
|
|
141
|
+
<>{children}</>
|
|
142
|
+
</LuiModalAsyncContext.Provider>
|
|
136
143
|
);
|
|
137
144
|
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { LuiModalAsyncInstanceContext } from "./LuiModalAsyncInstanceContext";
|
|
2
|
+
import React, { PropsWithChildren, ReactElement, useContext } from "react";
|
|
3
|
+
|
|
4
|
+
import { LuiButton, LuiIcon } from "@linzjs/lui";
|
|
5
|
+
import { LuiIconName } from "@linzjs/lui/dist/assets/svg-content";
|
|
6
|
+
|
|
7
|
+
export interface LuiModalAsyncHeaderProps {
|
|
8
|
+
icon?: LuiIconName | ReactElement;
|
|
9
|
+
title: string;
|
|
10
|
+
helpLink?: string;
|
|
11
|
+
onHelpClick?: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generic modal header.
|
|
16
|
+
*/
|
|
17
|
+
export const LuiModalAsyncHeader = ({
|
|
18
|
+
icon,
|
|
19
|
+
title,
|
|
20
|
+
helpLink,
|
|
21
|
+
onHelpClick,
|
|
22
|
+
}: PropsWithChildren<LuiModalAsyncHeaderProps>) => {
|
|
23
|
+
const { ownerRef } = useContext(LuiModalAsyncInstanceContext);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className={"LuiModalAsync-header"}>
|
|
27
|
+
{icon &&
|
|
28
|
+
(typeof icon === "string" ? (
|
|
29
|
+
<LuiIcon
|
|
30
|
+
name={icon}
|
|
31
|
+
alt={"icon"}
|
|
32
|
+
size={"md"}
|
|
33
|
+
className={`LuiModalAsync-header-icon LuiModalAsync-header-icon-${icon}`}
|
|
34
|
+
/>
|
|
35
|
+
) : (
|
|
36
|
+
icon
|
|
37
|
+
))}
|
|
38
|
+
<h4>{title}</h4>
|
|
39
|
+
{(helpLink || onHelpClick) && (
|
|
40
|
+
<LuiButton
|
|
41
|
+
type={"button"}
|
|
42
|
+
level={"plain-text"}
|
|
43
|
+
aria-label={"Help"}
|
|
44
|
+
title={"Help"}
|
|
45
|
+
className={"lui-button-icon-only"}
|
|
46
|
+
buttonProps={{ "data-noautofocus": true }}
|
|
47
|
+
onClick={
|
|
48
|
+
helpLink
|
|
49
|
+
? () => (ownerRef.current?.ownerDocument?.defaultView ?? window).open(helpLink, "_blank")
|
|
50
|
+
: onHelpClick
|
|
51
|
+
}
|
|
52
|
+
>
|
|
53
|
+
<LuiIcon name="ic_help_outline" alt="Help" size="md" />
|
|
54
|
+
</LuiButton>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { MutableRefObject, createContext } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Actions that can be taken from within the modal via instance context
|
|
5
|
+
*/
|
|
6
|
+
export interface LuiModalAsyncInstanceContextType {
|
|
7
|
+
ownerRef: MutableRefObject<HTMLElement | null>;
|
|
8
|
+
close: () => void;
|
|
9
|
+
resolve: (value: any) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const NoContextError = () => {
|
|
13
|
+
console.error("Missing LuiModalAsyncInstanceContext Provider");
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Provides access to resolving/closing to modal elements.
|
|
18
|
+
*/
|
|
19
|
+
export const LuiModalAsyncInstanceContext = createContext<LuiModalAsyncInstanceContextType>({
|
|
20
|
+
ownerRef: { current: null },
|
|
21
|
+
close: NoContextError,
|
|
22
|
+
resolve: NoContextError,
|
|
23
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React, { PropsWithChildren } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wraps the content and header. This is just to match the way figma is arranged.
|
|
5
|
+
*/
|
|
6
|
+
export const LuiModalAsyncMain = ({ children }: PropsWithChildren<unknown>) => (
|
|
7
|
+
<div className={"LuiModalAsync-main"}>{children}</div>
|
|
8
|
+
);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./LuiModalAsync";
|
|
2
|
+
export * from "./LuiModalAsyncButtonGroup";
|
|
3
|
+
export * from "./LuiModalAsyncContent";
|
|
4
|
+
export * from "./LuiModalAsyncHeader";
|
|
5
|
+
export * from "./LuiModalAsyncMain";
|
|
6
|
+
export * from "./LuiModalAsyncContext";
|
|
7
|
+
export * from "./LuiModalAsyncContextProvider";
|
|
8
|
+
export * from "./LuiModalAsyncInstanceContext";
|
|
9
|
+
export * from "./useShowAsyncModal";
|
|
10
|
+
export * from "./useLuiModalPrefab";
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { LuiModalAsync, LuiModalAsyncProps } from "./LuiModalAsync";
|
|
2
|
+
import { LuiModalAsyncButtonGroup } from "./LuiModalAsyncButtonGroup";
|
|
3
|
+
import { LuiModalAsyncContent } from "./LuiModalAsyncContent";
|
|
4
|
+
import { LuiModalAsyncCallback, PromiseWithResolve } from "./LuiModalAsyncContext";
|
|
5
|
+
import { LuiModalAsyncHeader } from "./LuiModalAsyncHeader";
|
|
6
|
+
import { LuiModalAsyncMain } from "./LuiModalAsyncMain";
|
|
7
|
+
import { useShowAsyncModal } from "./useShowAsyncModal";
|
|
8
|
+
import clsx from "clsx";
|
|
9
|
+
import { flatMap } from "lodash";
|
|
10
|
+
import React, { PropsWithChildren, ReactElement, useCallback, useMemo, useState } from "react";
|
|
11
|
+
|
|
12
|
+
import { LuiButton, LuiCheckboxInput, LuiMiniSpinner } from "@linzjs/lui";
|
|
13
|
+
import { LuiButtonProps } from "@linzjs/lui/dist/components/LuiButton/LuiButton";
|
|
14
|
+
import { IconName } from "@linzjs/lui/dist/components/LuiIcon/LuiIcon";
|
|
15
|
+
|
|
16
|
+
export type PrefabType = "success" | "info" | "warning" | "error" | "progress";
|
|
17
|
+
|
|
18
|
+
export interface LuiModalAsyncPrefabButton<RT> {
|
|
19
|
+
default?: boolean;
|
|
20
|
+
level?: LuiButtonProps["level"];
|
|
21
|
+
title: string;
|
|
22
|
+
value?: RT;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface useLuiModalPrefabProps<RT extends any = string> extends LuiModalAsyncProps {
|
|
26
|
+
level: PrefabType;
|
|
27
|
+
title: string;
|
|
28
|
+
helpLink?: string;
|
|
29
|
+
onHelpClick?: () => void;
|
|
30
|
+
buttons?: LuiModalAsyncPrefabButton<RT>[];
|
|
31
|
+
dontShowAgainSessionKey?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface LuiModalAsyncPrefabProps<RT> extends useLuiModalPrefabProps<RT>, LuiModalAsyncCallback<RT> {
|
|
35
|
+
//
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const getIconForLevel = (level: PrefabType): IconName | "custom_progress" => {
|
|
39
|
+
switch (level) {
|
|
40
|
+
case "success":
|
|
41
|
+
return "ic_check_circle_outline";
|
|
42
|
+
case "info":
|
|
43
|
+
return "ic_info_outline";
|
|
44
|
+
case "warning":
|
|
45
|
+
return "ic_warning_outline";
|
|
46
|
+
case "error":
|
|
47
|
+
return "ic_error_outline";
|
|
48
|
+
case "progress":
|
|
49
|
+
return "custom_progress";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const LuiModalDontShowSessionStorageKey = (userKey: string): string => `__LuiModalContext_dontshowagain_${userKey}`;
|
|
54
|
+
|
|
55
|
+
export const LuiModalDontShowSessionSet = (userKey: string): void =>
|
|
56
|
+
window.sessionStorage.setItem(LuiModalDontShowSessionStorageKey(userKey), "true");
|
|
57
|
+
|
|
58
|
+
export const LuiModalDontShowSessionRemove = (userKey: string): void =>
|
|
59
|
+
window.sessionStorage.removeItem(LuiModalDontShowSessionStorageKey(userKey));
|
|
60
|
+
|
|
61
|
+
export const LuiModalDontShowSessionCheck = (userKey: string): boolean =>
|
|
62
|
+
!!window.sessionStorage.getItem(LuiModalDontShowSessionStorageKey(userKey));
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A generic template for common types of modals.
|
|
66
|
+
*/
|
|
67
|
+
export const LuiModalPrefab = (props: PropsWithChildren<LuiModalAsyncPrefabProps<any>>): ReactElement => {
|
|
68
|
+
const {
|
|
69
|
+
level = "warning",
|
|
70
|
+
className,
|
|
71
|
+
style,
|
|
72
|
+
title,
|
|
73
|
+
helpLink,
|
|
74
|
+
onHelpClick,
|
|
75
|
+
children,
|
|
76
|
+
resolve,
|
|
77
|
+
closeOnOverlayClick,
|
|
78
|
+
dontShowAgainSessionKey,
|
|
79
|
+
} = props;
|
|
80
|
+
const [dontShowAgain, setDontShowAgain] = useState(false);
|
|
81
|
+
|
|
82
|
+
const icon = getIconForLevel(level);
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Default buttons
|
|
86
|
+
* Warning, Info, Error, Success: "Dismiss"
|
|
87
|
+
* Progress: "Cancel"
|
|
88
|
+
*/
|
|
89
|
+
const buttons: LuiModalAsyncPrefabButton<any>[] = props.buttons ?? [
|
|
90
|
+
{
|
|
91
|
+
title: level === "progress" ? "Cancel" : "Dismiss",
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
// If there are two options, option 1 is secondary, option 2 is primary
|
|
96
|
+
// Otherwise all options are secondary
|
|
97
|
+
buttons.length === 2 &&
|
|
98
|
+
buttons.forEach((b, i) => {
|
|
99
|
+
if (!b.level) {
|
|
100
|
+
// Last button is primary all others are secondary
|
|
101
|
+
b.level = i === buttons.length - 1 ? "primary" : "secondary";
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* When dialog closes save the "don't show" property
|
|
107
|
+
*/
|
|
108
|
+
const setDontShow = () => {
|
|
109
|
+
if (dontShowAgainSessionKey && dontShowAgain) {
|
|
110
|
+
LuiModalDontShowSessionSet(dontShowAgainSessionKey);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<LuiModalAsync
|
|
116
|
+
className={clsx("LuiModalPrefab", `LuiModalPrefab-${level}`, className)}
|
|
117
|
+
style={style}
|
|
118
|
+
closeOnOverlayClick={closeOnOverlayClick}
|
|
119
|
+
>
|
|
120
|
+
<LuiModalAsyncMain>
|
|
121
|
+
<LuiModalAsyncHeader
|
|
122
|
+
title={title}
|
|
123
|
+
icon={icon === "custom_progress" ? <LuiMiniSpinner size={24} /> : icon}
|
|
124
|
+
helpLink={helpLink}
|
|
125
|
+
onHelpClick={onHelpClick}
|
|
126
|
+
/>
|
|
127
|
+
<LuiModalAsyncContent>{children}</LuiModalAsyncContent>
|
|
128
|
+
{dontShowAgainSessionKey && (
|
|
129
|
+
<LuiCheckboxInput
|
|
130
|
+
className={"LuiCheckboxInput--nomargin"}
|
|
131
|
+
label={"Don't show me this again"}
|
|
132
|
+
value={"dontShowAgain"}
|
|
133
|
+
isChecked={dontShowAgain}
|
|
134
|
+
onChange={(e) => setDontShowAgain(e.target.checked)}
|
|
135
|
+
/>
|
|
136
|
+
)}
|
|
137
|
+
</LuiModalAsyncMain>
|
|
138
|
+
<LuiModalAsyncButtonGroup>
|
|
139
|
+
{buttons.map((pf, i) => (
|
|
140
|
+
<LuiButton
|
|
141
|
+
key={i}
|
|
142
|
+
level={pf.level ?? "secondary"}
|
|
143
|
+
onClick={() => {
|
|
144
|
+
setDontShow();
|
|
145
|
+
resolve(pf.value);
|
|
146
|
+
}}
|
|
147
|
+
buttonProps={pf.default ? { "data-autofocus": true } : undefined}
|
|
148
|
+
>
|
|
149
|
+
{pf.title}
|
|
150
|
+
</LuiButton>
|
|
151
|
+
))}
|
|
152
|
+
</LuiModalAsyncButtonGroup>
|
|
153
|
+
</LuiModalAsync>
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Instantiate a generic template for common types of modals.
|
|
159
|
+
*
|
|
160
|
+
* Usage:
|
|
161
|
+
* const { modalOwnerRef, showPrefabModal } = useLuiModalPrefab();
|
|
162
|
+
*
|
|
163
|
+
* return (
|
|
164
|
+
* <button
|
|
165
|
+
* ref={modalOwnerRef}
|
|
166
|
+
* onClick={() =>
|
|
167
|
+
* showPrefabModal({
|
|
168
|
+
* level: 'info',
|
|
169
|
+
* title: 'You are a fantastic person',
|
|
170
|
+
* children: 'Keep it up!',
|
|
171
|
+
* })
|
|
172
|
+
* }>
|
|
173
|
+
* Open Modal
|
|
174
|
+
* </button>
|
|
175
|
+
*/
|
|
176
|
+
export const useLuiModalPrefab = () => {
|
|
177
|
+
const { showModal, modalOwnerRef } = useShowAsyncModal();
|
|
178
|
+
|
|
179
|
+
const showPrefabModal = useCallback(
|
|
180
|
+
<RT extends any = string>(props: PropsWithChildren<useLuiModalPrefabProps<RT>>) => {
|
|
181
|
+
if (typeof props.children === "string") {
|
|
182
|
+
// Convert \n to <br/>
|
|
183
|
+
const split = props.children.split("\n");
|
|
184
|
+
props.children = flatMap(split.map((part, i) => (i !== split.length - 1 ? [part, <br key={i} />] : part)));
|
|
185
|
+
}
|
|
186
|
+
if (props.dontShowAgainSessionKey && LuiModalDontShowSessionCheck(props.dontShowAgainSessionKey)) {
|
|
187
|
+
return Promise.resolve(undefined);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return showModal(LuiModalPrefab, props) as PromiseWithResolve<RT>;
|
|
191
|
+
},
|
|
192
|
+
[showModal],
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
return useMemo(
|
|
196
|
+
() => ({
|
|
197
|
+
modalOwnerRef,
|
|
198
|
+
showPrefabModal,
|
|
199
|
+
}),
|
|
200
|
+
[modalOwnerRef, showPrefabModal],
|
|
201
|
+
);
|
|
202
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ComponentType, LuiModalAsyncContext, PromiseWithResolve } from "./LuiModalAsyncContext";
|
|
2
|
+
import { ComponentProps, useCallback, useContext, useMemo, useRef } from "react";
|
|
3
|
+
|
|
4
|
+
export const useShowAsyncModal = () => {
|
|
5
|
+
const { showModal } = useContext(LuiModalAsyncContext);
|
|
6
|
+
|
|
7
|
+
const modalOwnerRef = useRef<any>(null);
|
|
8
|
+
|
|
9
|
+
const showModalAsync = useCallback(
|
|
10
|
+
<CT extends ComponentType>(
|
|
11
|
+
component: CT,
|
|
12
|
+
args: Omit<ComponentProps<CT>, "resolve" | "close">,
|
|
13
|
+
): PromiseWithResolve<Parameters<ComponentProps<CT>["resolve"]>[0]> => showModal(modalOwnerRef, component, args),
|
|
14
|
+
[showModal],
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
return useMemo(
|
|
18
|
+
() => ({
|
|
19
|
+
showModal: showModalAsync,
|
|
20
|
+
modalOwnerRef,
|
|
21
|
+
}),
|
|
22
|
+
[showModalAsync],
|
|
23
|
+
);
|
|
24
|
+
};
|
package/dist/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from "./
|
|
1
|
+
export * from "./LuiModalAsync";
|
|
2
2
|
export * from "./panel";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { useInterval } from "../util/useInterval";
|
|
2
1
|
import { PanelInstanceContextProvider } from "./PanelInstanceContextProvider";
|
|
3
2
|
import { PanelInstance, PanelPosition, PanelsContext } from "./PanelsContext";
|
|
4
3
|
import { castArray, maxBy, sortBy } from "lodash-es";
|
|
5
4
|
import React, { Fragment, PropsWithChildren, ReactElement, useCallback, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { useInterval } from "usehooks-ts";
|
|
6
6
|
|
|
7
7
|
export interface PanelsContextProviderProps {
|
|
8
8
|
baseZIndex?: number;
|
package/package.json
CHANGED
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
"popout"
|
|
14
14
|
],
|
|
15
15
|
"main": "./dist/index.ts",
|
|
16
|
-
"version": "
|
|
16
|
+
"version": "2.0.1",
|
|
17
17
|
"peerDependencies": {
|
|
18
|
-
"@linzjs/lui": "^
|
|
18
|
+
"@linzjs/lui": "^18",
|
|
19
19
|
"lodash-es": ">=4",
|
|
20
20
|
"react": ">=17",
|
|
21
21
|
"react-dom": ">=17"
|
|
@@ -49,71 +49,72 @@
|
|
|
49
49
|
"@emotion/cache": "^11.11.0",
|
|
50
50
|
"@emotion/react": "^11.11.1",
|
|
51
51
|
"@emotion/styled": "^11.11.0",
|
|
52
|
-
"@linzjs/lui": "^
|
|
52
|
+
"@linzjs/lui": "^20.0.3",
|
|
53
53
|
"lodash-es": ">=4",
|
|
54
|
-
"react": ">=
|
|
55
|
-
"react-dom": ">=
|
|
54
|
+
"react": ">=18",
|
|
55
|
+
"react-dom": ">=18",
|
|
56
56
|
"react-rnd": "^10.4.1",
|
|
57
|
-
"
|
|
57
|
+
"usehooks-ts": "^2.9.1",
|
|
58
|
+
"uuid": "^9.0.1"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
60
61
|
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
|
61
62
|
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
|
62
|
-
"@linzjs/step-ag-grid": "^
|
|
63
|
-
"@rollup/plugin-commonjs": "^25.0.
|
|
63
|
+
"@linzjs/step-ag-grid": "^17.6.1",
|
|
64
|
+
"@rollup/plugin-commonjs": "^25.0.4",
|
|
64
65
|
"@rollup/plugin-json": "^6.0.0",
|
|
65
|
-
"@rollup/plugin-node-resolve": "^15.1
|
|
66
|
-
"@storybook/addon-docs": "^7.
|
|
67
|
-
"@storybook/addon-essentials": "^7.
|
|
68
|
-
"@storybook/addon-interactions": "^7.
|
|
69
|
-
"@storybook/addon-links": "^7.
|
|
70
|
-
"@storybook/
|
|
71
|
-
"@storybook/
|
|
72
|
-
"@storybook/jest": "^0.
|
|
73
|
-
"@storybook/preset-create-react-app": "^7.
|
|
74
|
-
"@storybook/react": "^7.
|
|
75
|
-
"@storybook/react-vite": "^7.
|
|
76
|
-
"@storybook/test-runner": "^0.
|
|
77
|
-
"@storybook/testing-library": "^0.2.
|
|
78
|
-
"@testing-library/jest-dom": "^
|
|
66
|
+
"@rollup/plugin-node-resolve": "^15.2.1",
|
|
67
|
+
"@storybook/addon-docs": "^7.4.5",
|
|
68
|
+
"@storybook/addon-essentials": "^7.4.5",
|
|
69
|
+
"@storybook/addon-interactions": "^7.4.5",
|
|
70
|
+
"@storybook/addon-links": "^7.4.5",
|
|
71
|
+
"@storybook/addon-mdx-gfm": "^7.4.5",
|
|
72
|
+
"@storybook/blocks": "^7.4.5",
|
|
73
|
+
"@storybook/jest": "^0.2.2",
|
|
74
|
+
"@storybook/preset-create-react-app": "^7.4.5",
|
|
75
|
+
"@storybook/react": "^7.4.5",
|
|
76
|
+
"@storybook/react-vite": "^7.4.5",
|
|
77
|
+
"@storybook/test-runner": "^0.13.0",
|
|
78
|
+
"@storybook/testing-library": "^0.2.1",
|
|
79
|
+
"@testing-library/jest-dom": "^6.1.3",
|
|
79
80
|
"@testing-library/react": "^14.0.0",
|
|
80
|
-
"@testing-library/user-event": "^14.
|
|
81
|
-
"@trivago/prettier-plugin-sort-imports": "^4.
|
|
82
|
-
"@types/jest": "^29.5.
|
|
83
|
-
"@types/lodash-es": "^4.17.
|
|
84
|
-
"@types/node": "^20.
|
|
85
|
-
"@types/react": "^18.2.
|
|
86
|
-
"@types/react-dom": "^18.2.
|
|
87
|
-
"@types/uuid": "^9.0.
|
|
88
|
-
"ag-grid-community": "^
|
|
89
|
-
"ag-grid-react": "^
|
|
90
|
-
"eslint": "^8.
|
|
91
|
-
"eslint-config-prettier": "^8.
|
|
81
|
+
"@testing-library/user-event": "^14.5.1",
|
|
82
|
+
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
|
|
83
|
+
"@types/jest": "^29.5.5",
|
|
84
|
+
"@types/lodash-es": "^4.17.9",
|
|
85
|
+
"@types/node": "^20.7.1",
|
|
86
|
+
"@types/react": "^18.2.23",
|
|
87
|
+
"@types/react-dom": "^18.2.8",
|
|
88
|
+
"@types/uuid": "^9.0.4",
|
|
89
|
+
"ag-grid-community": "^29.3.5",
|
|
90
|
+
"ag-grid-react": "^29.3.5",
|
|
91
|
+
"eslint": "^8.50.0",
|
|
92
|
+
"eslint-config-prettier": "^8.10.0",
|
|
92
93
|
"eslint-config-react-app": "^7.0.1",
|
|
93
|
-
"eslint-plugin-deprecation": "^1.
|
|
94
|
-
"eslint-plugin-import": "^2.
|
|
95
|
-
"eslint-plugin-jest": "^27.
|
|
94
|
+
"eslint-plugin-deprecation": "^1.6.0",
|
|
95
|
+
"eslint-plugin-import": "^2.28.1",
|
|
96
|
+
"eslint-plugin-jest": "^27.4.0",
|
|
96
97
|
"eslint-plugin-jsx-a11y": "^6.7.1",
|
|
97
98
|
"eslint-plugin-prettier": "^4.2.1",
|
|
98
|
-
"eslint-plugin-react": "^7.
|
|
99
|
+
"eslint-plugin-react": "^7.33.2",
|
|
99
100
|
"eslint-plugin-react-hooks": "^4.6.0",
|
|
100
|
-
"eslint-plugin-storybook": "^0.6.
|
|
101
|
-
"eslint-plugin-testing-library": "^5.11.
|
|
102
|
-
"jest": "^29.
|
|
101
|
+
"eslint-plugin-storybook": "^0.6.14",
|
|
102
|
+
"eslint-plugin-testing-library": "^5.11.1",
|
|
103
|
+
"jest": "^29.7.0",
|
|
103
104
|
"jest-canvas-mock": "^2.5.2",
|
|
104
|
-
"jest-environment-jsdom": "^29.
|
|
105
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
105
106
|
"jest-expect-message": "^1.1.3",
|
|
106
107
|
"mkdirp": "^3.0.1",
|
|
107
108
|
"npm-run-all": "^4.1.5",
|
|
108
109
|
"prettier": "^2.8.8",
|
|
109
110
|
"prop-types": "^15.8.1",
|
|
110
111
|
"react-scripts": "5.0.1",
|
|
111
|
-
"rollup": "^3.
|
|
112
|
-
"rollup-plugin-copy": "^3.
|
|
113
|
-
"sass": "^1.
|
|
112
|
+
"rollup": "^3.29.3",
|
|
113
|
+
"rollup-plugin-copy": "^3.5.0",
|
|
114
|
+
"sass": "^1.68.0",
|
|
114
115
|
"sass-loader": "^13.3.2",
|
|
115
116
|
"semantic-release": "^19.0.5",
|
|
116
|
-
"storybook": "^7.
|
|
117
|
+
"storybook": "^7.4.5",
|
|
117
118
|
"style-loader": "^3.3.3",
|
|
118
119
|
"stylelint": "^14.16.1",
|
|
119
120
|
"stylelint-config-prettier": "^9.0.5",
|
|
@@ -122,7 +123,7 @@
|
|
|
122
123
|
"stylelint-prettier": "3.0.0",
|
|
123
124
|
"stylelint-scss": "5.0.1",
|
|
124
125
|
"typescript": "^4.9.5",
|
|
125
|
-
"vite": "^4.4.
|
|
126
|
+
"vite": "^4.4.9"
|
|
126
127
|
},
|
|
127
128
|
"eslintConfig": {
|
|
128
129
|
"extends": [
|
package/dist/modal/Modal.tsx
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { ModalInstanceContext } from "./ModalInstanceContext";
|
|
2
|
-
import clsx from "clsx";
|
|
3
|
-
import { delay } from "lodash-es";
|
|
4
|
-
import React, { PropsWithChildren, ReactElement, useContext, useEffect, useRef } from "react";
|
|
5
|
-
|
|
6
|
-
export interface ModalProps {
|
|
7
|
-
className?: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const Modal = ({ className, children }: PropsWithChildren<ModalProps>): ReactElement => {
|
|
11
|
-
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
12
|
-
|
|
13
|
-
const { close } = useContext(ModalInstanceContext);
|
|
14
|
-
|
|
15
|
-
// The only way to create a modal dialog is to call showModal, open attribute will not work
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
// Check if it's open already to support .vite hot deploys
|
|
18
|
-
if (!dialogRef.current?.open) {
|
|
19
|
-
dialogRef.current?.showModal();
|
|
20
|
-
}
|
|
21
|
-
}, []);
|
|
22
|
-
|
|
23
|
-
useEffect(() => {
|
|
24
|
-
// Dialogs auto select the first focusable element, this is in case you don't want that
|
|
25
|
-
delay(() => {
|
|
26
|
-
const input = dialogRef.current?.querySelectorAll("[data-autofocus]") as any;
|
|
27
|
-
input[0]?.focus?.();
|
|
28
|
-
input?.select?.();
|
|
29
|
-
}, 100);
|
|
30
|
-
}, []);
|
|
31
|
-
|
|
32
|
-
return (
|
|
33
|
-
<dialog
|
|
34
|
-
className={clsx("linzjs-windows-dialog", className)}
|
|
35
|
-
ref={dialogRef}
|
|
36
|
-
onClick={(e) => e.target === e.currentTarget && close()}
|
|
37
|
-
onCancel={(e) => {
|
|
38
|
-
e.preventDefault();
|
|
39
|
-
close();
|
|
40
|
-
}}
|
|
41
|
-
>
|
|
42
|
-
{children}
|
|
43
|
-
</dialog>
|
|
44
|
-
);
|
|
45
|
-
};
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { ComponentProps, MutableRefObject, ReactElement, createContext } from "react";
|
|
2
|
-
|
|
3
|
-
export type ComponentType = (props: any) => ReactElement<any, any>;
|
|
4
|
-
|
|
5
|
-
export interface ModalCallback<R> {
|
|
6
|
-
resolve: (result: R | undefined) => void;
|
|
7
|
-
close: () => void;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface ModalContextType {
|
|
11
|
-
showModal: <
|
|
12
|
-
OR extends HTMLElement | null,
|
|
13
|
-
PROPS extends ModalCallback<any>,
|
|
14
|
-
CT extends (props: PROPS) => ReactElement<any, any>,
|
|
15
|
-
RT = Parameters<Parameters<CT>[0]["resolve"]>[0],
|
|
16
|
-
>(
|
|
17
|
-
ownerRef: MutableRefObject<OR>,
|
|
18
|
-
component: CT,
|
|
19
|
-
args: Omit<ComponentProps<CT>, "resolve" | "close">,
|
|
20
|
-
) => Promise<RT>;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export const ModalContext = createContext<ModalContextType>({
|
|
24
|
-
showModal: (async () => {
|
|
25
|
-
console.error("Missing ModalContext Provider");
|
|
26
|
-
}) as ModalContextType["showModal"],
|
|
27
|
-
});
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { createContext } from "react";
|
|
2
|
-
|
|
3
|
-
export interface ModalInstanceContextType {
|
|
4
|
-
close: () => void;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
const NoContextError = () => {
|
|
8
|
-
console.error("Missing ModalInstanceContext Provider");
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Provides access to resolving/closing to modal elements.
|
|
13
|
-
*/
|
|
14
|
-
export const ModalInstanceContext = createContext<ModalInstanceContextType>({
|
|
15
|
-
close: NoContextError,
|
|
16
|
-
});
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
@use "node_modules/@linzjs/lui/dist/scss/Foundation/Variables/ColorVars.scss" as colours;
|
|
2
|
-
|
|
3
|
-
dialog.linzjs-windows-dialog {
|
|
4
|
-
display: flex;
|
|
5
|
-
flex-direction: column;
|
|
6
|
-
padding: 0;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
dialog.prefab-modal {
|
|
10
|
-
border-radius: 5px;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
dialog.prefab-modal .lui-modal {
|
|
14
|
-
margin: 0 !important;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
dialog.prefab-modal .lui-button:focus {
|
|
18
|
-
outline: 2px solid colours.$brand-primary;
|
|
19
|
-
// make sure the button sits above the others so the outline is not cut off on focus
|
|
20
|
-
position: relative;
|
|
21
|
-
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import "./PrefabModal.scss";
|
|
2
|
-
|
|
3
|
-
import { Modal } from "./Modal";
|
|
4
|
-
import { ModalCallback } from "./ModalContext";
|
|
5
|
-
import { useShowModal } from "./useShowModal";
|
|
6
|
-
import { isEmpty } from "lodash-es";
|
|
7
|
-
import React, { ReactElement } from "react";
|
|
8
|
-
|
|
9
|
-
import { LuiAlertModalButtons, LuiButton, LuiIcon } from "@linzjs/lui";
|
|
10
|
-
import { LuiButtonProps } from "@linzjs/lui/dist/components/LuiButton/LuiButton";
|
|
11
|
-
import { IconName } from "@linzjs/lui/dist/components/LuiIcon/LuiIcon";
|
|
12
|
-
|
|
13
|
-
export type WarningLevel = "success" | "info" | "warning" | "error";
|
|
14
|
-
|
|
15
|
-
export interface PrefabButton<RT> {
|
|
16
|
-
default?: boolean;
|
|
17
|
-
level?: LuiButtonProps["level"];
|
|
18
|
-
title: string;
|
|
19
|
-
value: RT;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface PrefabModalProps extends ModalCallback<string | null> {
|
|
23
|
-
level?: WarningLevel;
|
|
24
|
-
children: ReactElement;
|
|
25
|
-
buttons?: PrefabButton<any>[];
|
|
26
|
-
}
|
|
27
|
-
export const getIconForLevel = (level: "success" | "info" | "warning" | "error"): IconName => {
|
|
28
|
-
switch (level) {
|
|
29
|
-
case "success":
|
|
30
|
-
return "ic_check_circle";
|
|
31
|
-
case "info":
|
|
32
|
-
return "ic_info";
|
|
33
|
-
case "warning":
|
|
34
|
-
return "ic_warning";
|
|
35
|
-
case "error":
|
|
36
|
-
return "ic_error";
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export const PrefabModal = ({ level = "warning", children, resolve, ...props }: PrefabModalProps) => {
|
|
41
|
-
const icon = getIconForLevel(level);
|
|
42
|
-
const buttons: PrefabButton<any>[] = props.buttons ?? [];
|
|
43
|
-
if (isEmpty(buttons)) {
|
|
44
|
-
if (level === "warning") {
|
|
45
|
-
buttons.push({
|
|
46
|
-
title: "Cancel",
|
|
47
|
-
value: null,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
buttons.push({
|
|
51
|
-
title: "Continue",
|
|
52
|
-
value: true,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
buttons.forEach((b, i) => {
|
|
57
|
-
if (!b.level) {
|
|
58
|
-
b.level = i === buttons.length - 1 ? "primary" : "secondary";
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
return (
|
|
63
|
-
<Modal className={"prefab-modal"}>
|
|
64
|
-
<div className={`lui-modal lui-box-shadow lui-modal-${level}`} style={{ minWidth: 400 }}>
|
|
65
|
-
<LuiIcon name={icon} alt={"warning"} size={"lg"} className={"lui-msg-status-icon"} />
|
|
66
|
-
{children}
|
|
67
|
-
<LuiAlertModalButtons>
|
|
68
|
-
{buttons.map((pf, i) => (
|
|
69
|
-
<LuiButton
|
|
70
|
-
key={i}
|
|
71
|
-
level={pf.level}
|
|
72
|
-
onClick={() => resolve(pf.value === undefined ? pf.title : pf.value)}
|
|
73
|
-
buttonProps={pf.default ? { "data-autofocus": true } : undefined}
|
|
74
|
-
>
|
|
75
|
-
{pf.title}
|
|
76
|
-
</LuiButton>
|
|
77
|
-
))}
|
|
78
|
-
</LuiAlertModalButtons>
|
|
79
|
-
</div>
|
|
80
|
-
</Modal>
|
|
81
|
-
);
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
export const usePrefabModal = () => {
|
|
85
|
-
const { showModal, modalOwnerRef } = useShowModal();
|
|
86
|
-
return {
|
|
87
|
-
modalOwnerRef,
|
|
88
|
-
showPrefabModal: <RT extends any = boolean>(
|
|
89
|
-
level: WarningLevel,
|
|
90
|
-
title: ReactElement | string,
|
|
91
|
-
content: ReactElement | string,
|
|
92
|
-
buttons?: PrefabButton<RT>[],
|
|
93
|
-
) =>
|
|
94
|
-
showModal(PrefabModal, {
|
|
95
|
-
level,
|
|
96
|
-
|
|
97
|
-
children: (
|
|
98
|
-
<>
|
|
99
|
-
{typeof title === "string" ? <h2>{title}</h2> : title}
|
|
100
|
-
{typeof content === "string" ? <p>{content}</p> : content}
|
|
101
|
-
</>
|
|
102
|
-
),
|
|
103
|
-
buttons,
|
|
104
|
-
}) as Promise<RT>,
|
|
105
|
-
};
|
|
106
|
-
};
|
package/dist/modal/index.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { ComponentType, ModalContext } from "./ModalContext";
|
|
2
|
-
import { ComponentProps, useContext, useRef } from "react";
|
|
3
|
-
|
|
4
|
-
export const useShowModal = () => {
|
|
5
|
-
const { showModal } = useContext(ModalContext);
|
|
6
|
-
|
|
7
|
-
const modalOwnerRef = useRef<any>(null);
|
|
8
|
-
|
|
9
|
-
return {
|
|
10
|
-
showModal: <CT extends ComponentType>(
|
|
11
|
-
component: CT,
|
|
12
|
-
args: Omit<ComponentProps<CT>, "resolve" | "close">,
|
|
13
|
-
): Promise<Parameters<Parameters<CT>[0]["resolve"]>[0]> => showModal(modalOwnerRef, component, args),
|
|
14
|
-
modalOwnerRef,
|
|
15
|
-
};
|
|
16
|
-
};
|
package/dist/util/useInterval.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef } from "react";
|
|
2
|
-
|
|
3
|
-
type Callback = () => void | Promise<void>;
|
|
4
|
-
|
|
5
|
-
export const useInterval = (callback: Callback, timeoutMs: number | null) => {
|
|
6
|
-
const callbackRef = useRef(callback);
|
|
7
|
-
callbackRef.current = callback;
|
|
8
|
-
|
|
9
|
-
useEffect(() => {
|
|
10
|
-
if (!timeoutMs) return;
|
|
11
|
-
const interval = setInterval(() => {
|
|
12
|
-
callbackRef.current && callbackRef.current();
|
|
13
|
-
}, timeoutMs);
|
|
14
|
-
return () => {
|
|
15
|
-
clearInterval(interval);
|
|
16
|
-
};
|
|
17
|
-
}, [timeoutMs]);
|
|
18
|
-
};
|