@stack-spot/portal-components 1.0.0-dev.1775743853760 → 1.0.0-dev.1778512145216
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/components/Modal/Dialog.d.ts +165 -0
- package/dist/components/Modal/Dialog.d.ts.map +1 -0
- package/dist/components/Modal/Dialog.js +170 -0
- package/dist/components/Modal/Dialog.js.map +1 -0
- package/dist/components/Modal/Modal.d.ts +70 -0
- package/dist/components/Modal/Modal.d.ts.map +1 -0
- package/dist/components/Modal/Modal.js +67 -0
- package/dist/components/Modal/Modal.js.map +1 -0
- package/dist/components/Modal/index.d.ts +5 -0
- package/dist/components/Modal/index.d.ts.map +1 -0
- package/dist/components/Modal/index.js +3 -0
- package/dist/components/Modal/index.js.map +1 -0
- package/dist/components/Modal/style.css +103 -0
- package/package.json +12 -8
- package/src/components/Modal/Dialog.tsx +318 -0
- package/src/components/Modal/Modal.tsx +110 -0
- package/src/components/Modal/index.ts +6 -0
- package/src/components/Modal/style.css +103 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { type AriaAttributes, type CSSProperties, type PropsWithChildren, type SyntheticEvent } from 'react';
|
|
2
|
+
import './style.css';
|
|
3
|
+
/**
|
|
4
|
+
* Available size options for the dialog.
|
|
5
|
+
*/
|
|
6
|
+
export type DialogSize = 'small' | 'medium' | 'large' | 'extra-large' | 'fit-content';
|
|
7
|
+
/**
|
|
8
|
+
* Type of dialog display.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* - `modal`: Centered dialog with backdrop
|
|
12
|
+
* - `right-panel`: Slide-in panel from the right side
|
|
13
|
+
*/
|
|
14
|
+
type DialogType = 'modal' | 'right-panel';
|
|
15
|
+
/**
|
|
16
|
+
* Props for controlled dialog state.
|
|
17
|
+
*/
|
|
18
|
+
type ControlledProps = {
|
|
19
|
+
/**
|
|
20
|
+
* Controls whether the dialog is open.
|
|
21
|
+
*/
|
|
22
|
+
isOpen: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Callback to update the dialog's open state.
|
|
25
|
+
*/
|
|
26
|
+
setIsOpen: (value: boolean) => void;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Props for uncontrolled dialog state.
|
|
30
|
+
*
|
|
31
|
+
* @remarks
|
|
32
|
+
* Use this when you want the dialog to manage its own state internally.
|
|
33
|
+
*/
|
|
34
|
+
type UncontrolledProps = {
|
|
35
|
+
isOpen?: never;
|
|
36
|
+
setIsOpen?: never;
|
|
37
|
+
};
|
|
38
|
+
type AriaProps = Pick<AriaAttributes, 'aria-labelledby' | 'aria-label' | 'aria-describedby' | 'aria-description'>;
|
|
39
|
+
export type DialogProps = PropsWithChildren & (ControlledProps | UncontrolledProps) & AriaProps & {
|
|
40
|
+
/**
|
|
41
|
+
* The display type of the dialog.
|
|
42
|
+
*
|
|
43
|
+
* @defaultValue 'modal'
|
|
44
|
+
*/
|
|
45
|
+
type?: DialogType;
|
|
46
|
+
/**
|
|
47
|
+
* Whether clicking outside the dialog should close it.
|
|
48
|
+
*
|
|
49
|
+
* @defaultValue true
|
|
50
|
+
*/
|
|
51
|
+
shouldLightDismiss?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Initial open state for uncontrolled dialogs.
|
|
54
|
+
*
|
|
55
|
+
* @defaultValue false
|
|
56
|
+
*/
|
|
57
|
+
initialOpen?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Callback fired when the dialog is closed.
|
|
60
|
+
*/
|
|
61
|
+
onClose?: (event?: SyntheticEvent) => void;
|
|
62
|
+
/**
|
|
63
|
+
* The size of the dialog.
|
|
64
|
+
*
|
|
65
|
+
* @defaultValue 'medium'
|
|
66
|
+
*/
|
|
67
|
+
size?: DialogSize;
|
|
68
|
+
/**
|
|
69
|
+
* Custom inline styles for the dialog element.
|
|
70
|
+
*/
|
|
71
|
+
style?: CSSProperties;
|
|
72
|
+
/**
|
|
73
|
+
* Additional CSS class names.
|
|
74
|
+
*/
|
|
75
|
+
className?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Optional identifier for the dialog element.
|
|
78
|
+
*/
|
|
79
|
+
id?: string;
|
|
80
|
+
};
|
|
81
|
+
export interface DialogRef extends Pick<HTMLDialogElement, 'addEventListener' | 'removeEventListener'> {
|
|
82
|
+
close: () => void;
|
|
83
|
+
showModal: () => void;
|
|
84
|
+
isOpen: () => boolean;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* A flexible dialog component that supports both modal and panel modes.
|
|
88
|
+
*
|
|
89
|
+
* @remarks
|
|
90
|
+
* The Dialog component provides a native HTML dialog element with enhanced functionality:
|
|
91
|
+
* - **Controlled or uncontrolled state**: Manage state externally or let the component handle it
|
|
92
|
+
* - **Light dismiss**: Optional backdrop click to close
|
|
93
|
+
* - **Keyboard support**: ESC key closes the dialog
|
|
94
|
+
* - **Multiple sizes**: Predefined size options
|
|
95
|
+
* - **Accessibility**: Full ARIA support
|
|
96
|
+
* - **Two display modes**: Modal (centered) or right panel (slide-in)
|
|
97
|
+
*
|
|
98
|
+
* **Important:** When using controlled mode, you must provide both `isOpen` and `setIsOpen` props.
|
|
99
|
+
* For uncontrolled mode, use the ref to control the dialog programmatically.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* Uncontrolled dialog with ref:
|
|
103
|
+
* ```tsx
|
|
104
|
+
* const dialogRef = useRef<DialogRef>(null)
|
|
105
|
+
*
|
|
106
|
+
* const handleOpen = () => {
|
|
107
|
+
* dialogRef.current?.showModal()
|
|
108
|
+
* }
|
|
109
|
+
*
|
|
110
|
+
* return (
|
|
111
|
+
* <>
|
|
112
|
+
* <button onClick={handleOpen}>Open Dialog</button>
|
|
113
|
+
* <Dialog
|
|
114
|
+
* ref={dialogRef}
|
|
115
|
+
* size="large"
|
|
116
|
+
* aria-label="Example dialog"
|
|
117
|
+
* >
|
|
118
|
+
* <Text>Dialog content</Text>
|
|
119
|
+
* </Dialog>
|
|
120
|
+
* </>
|
|
121
|
+
* )
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* Controlled dialog:
|
|
126
|
+
* ```tsx
|
|
127
|
+
* const [isOpen, setIsOpen] = useState(false)
|
|
128
|
+
*
|
|
129
|
+
* return (
|
|
130
|
+
* <>
|
|
131
|
+
* <button onClick={() => setIsOpen(true)}>Open Dialog</button>
|
|
132
|
+
* <Dialog
|
|
133
|
+
* isOpen={isOpen}
|
|
134
|
+
* setIsOpen={setIsOpen}
|
|
135
|
+
* type="right-panel"
|
|
136
|
+
* shouldLightDismiss={false}
|
|
137
|
+
* onClose={() => console.log('Dialog closed')}
|
|
138
|
+
* >
|
|
139
|
+
* <Text>Panel content</Text>
|
|
140
|
+
* </Dialog>
|
|
141
|
+
* </>
|
|
142
|
+
* )
|
|
143
|
+
* ```
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* Dialog with custom styling:
|
|
147
|
+
* ```tsx
|
|
148
|
+
* <Dialog
|
|
149
|
+
* size="fit-content"
|
|
150
|
+
* className="custom-dialog"
|
|
151
|
+
* style={{ maxWidth: '600px' }}
|
|
152
|
+
* aria-labelledby="dialog-title"
|
|
153
|
+
* >
|
|
154
|
+
* <h2 id="dialog-title">Custom Dialog</h2>
|
|
155
|
+
* <p>Content here</p>
|
|
156
|
+
* </Dialog>
|
|
157
|
+
* ```
|
|
158
|
+
*
|
|
159
|
+
* @param props - Dialog properties
|
|
160
|
+
* @param ref - Forwarded ref for imperative control
|
|
161
|
+
* @returns A dialog element
|
|
162
|
+
*/
|
|
163
|
+
export declare const Dialog: import("react").ForwardRefExoticComponent<DialogProps & import("react").RefAttributes<DialogRef>>;
|
|
164
|
+
export {};
|
|
165
|
+
//# sourceMappingURL=Dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Dialog.d.ts","sourceRoot":"","sources":["../../../src/components/Modal/Dialog.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EAMpB,MAAM,OAAO,CAAA;AACd,OAAO,aAAa,CAAA;AACpB;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,aAAa,GAAG,aAAa,CAAA;AAErF;;;;;;GAMG;AACH,KAAK,UAAU,GAAG,OAAO,GAAG,aAAa,CAAA;AAEzC;;GAEG;AACH,KAAK,eAAe,GAAG;IACrB;;OAEG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;OAEG;IACH,SAAS,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACrC,CAAA;AAED;;;;;GAKG;AACH,KAAK,iBAAiB,GAAG;IACvB,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,SAAS,CAAC,EAAE,KAAK,CAAC;CACnB,CAAA;AAED,KAAK,SAAS,GAAG,IAAI,CAAC,cAAc,EAAE,iBAAiB,GAAG,YAAY,GAAG,kBAAkB,GAAG,kBAAkB,CAAC,CAAA;AACjH,MAAM,MAAM,WAAW,GAAG,iBAAiB,GAAG,CAAC,eAAe,GAAG,iBAAiB,CAAC,GAAG,SAAS,GAAG;IAChG;;;;OAIG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;IAElB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAE7B;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,cAAc,KAAK,IAAI,CAAC;IAE3C;;;;OAIG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;IAElB;;OAEG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC;IAEtB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,CAAA;AAED,MAAM,WAAW,SAAU,SAAQ,IAAI,CAAC,iBAAiB,EAAE,kBAAkB,GAAG,qBAAqB,CAAC;IACpG,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,OAAO,CAAC;CACvB;AAmFD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4EG;AACH,eAAO,MAAM,MAAM,mGAgDlB,CAAA"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useImperativeHandle, useRef, useState, } from 'react';
|
|
3
|
+
import './style.css';
|
|
4
|
+
/**
|
|
5
|
+
* Custom hook for managing dialog state and behavior.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* This hook handles:
|
|
9
|
+
* - Opening and closing the dialog
|
|
10
|
+
* - Light dismiss (clicking outside to close)
|
|
11
|
+
* - Escape key handling
|
|
12
|
+
* - Cleanup on unmount
|
|
13
|
+
*
|
|
14
|
+
* @param props - Dialog configuration options
|
|
15
|
+
* @returns Dialog state and control methods
|
|
16
|
+
*/
|
|
17
|
+
const useDialog = ({ isOpen: controlledOpen, setIsOpen: setControlledOpen, initialOpen = false, shouldLightDismiss = true, }) => {
|
|
18
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(initialOpen);
|
|
19
|
+
const dialogRef = useRef(null);
|
|
20
|
+
const isOpen = controlledOpen ?? uncontrolledOpen;
|
|
21
|
+
const setIsOpen = setControlledOpen ?? setUncontrolledOpen;
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const dialog = dialogRef.current;
|
|
24
|
+
if (isOpen && dialog && !dialog.open) {
|
|
25
|
+
dialog.showModal();
|
|
26
|
+
}
|
|
27
|
+
else if (!isOpen) {
|
|
28
|
+
dialog?.close();
|
|
29
|
+
}
|
|
30
|
+
return () => {
|
|
31
|
+
dialog?.close();
|
|
32
|
+
};
|
|
33
|
+
}, [isOpen]);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const dialog = dialogRef.current;
|
|
36
|
+
if (!dialog)
|
|
37
|
+
return;
|
|
38
|
+
const handleClose = (event) => {
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
event.stopPropagation();
|
|
41
|
+
setIsOpen(false);
|
|
42
|
+
};
|
|
43
|
+
const lightDismiss = (event) => {
|
|
44
|
+
const { target } = event;
|
|
45
|
+
if (target instanceof Element && target.nodeName === 'DIALOG') {
|
|
46
|
+
handleClose(event);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const closeOnEscape = (event) => {
|
|
50
|
+
if (event.code === 'Escape') {
|
|
51
|
+
handleClose(event);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
if (shouldLightDismiss) {
|
|
55
|
+
dialog.addEventListener('click', lightDismiss);
|
|
56
|
+
}
|
|
57
|
+
dialog.addEventListener('keydown', closeOnEscape);
|
|
58
|
+
return () => {
|
|
59
|
+
if (shouldLightDismiss) {
|
|
60
|
+
dialog.removeEventListener('click', lightDismiss);
|
|
61
|
+
}
|
|
62
|
+
dialog.removeEventListener('keydown', closeOnEscape);
|
|
63
|
+
};
|
|
64
|
+
}, [shouldLightDismiss, setIsOpen]);
|
|
65
|
+
return {
|
|
66
|
+
dialogRef,
|
|
67
|
+
isOpen,
|
|
68
|
+
close: () => setIsOpen(false),
|
|
69
|
+
showModal: () => setIsOpen(true),
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* A flexible dialog component that supports both modal and panel modes.
|
|
74
|
+
*
|
|
75
|
+
* @remarks
|
|
76
|
+
* The Dialog component provides a native HTML dialog element with enhanced functionality:
|
|
77
|
+
* - **Controlled or uncontrolled state**: Manage state externally or let the component handle it
|
|
78
|
+
* - **Light dismiss**: Optional backdrop click to close
|
|
79
|
+
* - **Keyboard support**: ESC key closes the dialog
|
|
80
|
+
* - **Multiple sizes**: Predefined size options
|
|
81
|
+
* - **Accessibility**: Full ARIA support
|
|
82
|
+
* - **Two display modes**: Modal (centered) or right panel (slide-in)
|
|
83
|
+
*
|
|
84
|
+
* **Important:** When using controlled mode, you must provide both `isOpen` and `setIsOpen` props.
|
|
85
|
+
* For uncontrolled mode, use the ref to control the dialog programmatically.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* Uncontrolled dialog with ref:
|
|
89
|
+
* ```tsx
|
|
90
|
+
* const dialogRef = useRef<DialogRef>(null)
|
|
91
|
+
*
|
|
92
|
+
* const handleOpen = () => {
|
|
93
|
+
* dialogRef.current?.showModal()
|
|
94
|
+
* }
|
|
95
|
+
*
|
|
96
|
+
* return (
|
|
97
|
+
* <>
|
|
98
|
+
* <button onClick={handleOpen}>Open Dialog</button>
|
|
99
|
+
* <Dialog
|
|
100
|
+
* ref={dialogRef}
|
|
101
|
+
* size="large"
|
|
102
|
+
* aria-label="Example dialog"
|
|
103
|
+
* >
|
|
104
|
+
* <Text>Dialog content</Text>
|
|
105
|
+
* </Dialog>
|
|
106
|
+
* </>
|
|
107
|
+
* )
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* Controlled dialog:
|
|
112
|
+
* ```tsx
|
|
113
|
+
* const [isOpen, setIsOpen] = useState(false)
|
|
114
|
+
*
|
|
115
|
+
* return (
|
|
116
|
+
* <>
|
|
117
|
+
* <button onClick={() => setIsOpen(true)}>Open Dialog</button>
|
|
118
|
+
* <Dialog
|
|
119
|
+
* isOpen={isOpen}
|
|
120
|
+
* setIsOpen={setIsOpen}
|
|
121
|
+
* type="right-panel"
|
|
122
|
+
* shouldLightDismiss={false}
|
|
123
|
+
* onClose={() => console.log('Dialog closed')}
|
|
124
|
+
* >
|
|
125
|
+
* <Text>Panel content</Text>
|
|
126
|
+
* </Dialog>
|
|
127
|
+
* </>
|
|
128
|
+
* )
|
|
129
|
+
* ```
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* Dialog with custom styling:
|
|
133
|
+
* ```tsx
|
|
134
|
+
* <Dialog
|
|
135
|
+
* size="fit-content"
|
|
136
|
+
* className="custom-dialog"
|
|
137
|
+
* style={{ maxWidth: '600px' }}
|
|
138
|
+
* aria-labelledby="dialog-title"
|
|
139
|
+
* >
|
|
140
|
+
* <h2 id="dialog-title">Custom Dialog</h2>
|
|
141
|
+
* <p>Content here</p>
|
|
142
|
+
* </Dialog>
|
|
143
|
+
* ```
|
|
144
|
+
*
|
|
145
|
+
* @param props - Dialog properties
|
|
146
|
+
* @param ref - Forwarded ref for imperative control
|
|
147
|
+
* @returns A dialog element
|
|
148
|
+
*/
|
|
149
|
+
export const Dialog = forwardRef(({ type = 'modal', shouldLightDismiss = true, initialOpen = false, isOpen: controlledOpen, setIsOpen: setControlledOpen, onClose, children, size = 'medium', style, className, ...props }, forwardedRef) => {
|
|
150
|
+
const { dialogRef, isOpen, close, showModal } = useDialog({
|
|
151
|
+
isOpen: controlledOpen,
|
|
152
|
+
setIsOpen: setControlledOpen,
|
|
153
|
+
initialOpen,
|
|
154
|
+
shouldLightDismiss,
|
|
155
|
+
});
|
|
156
|
+
useImperativeHandle(forwardedRef, () => ({
|
|
157
|
+
close,
|
|
158
|
+
showModal,
|
|
159
|
+
isOpen: () => isOpen,
|
|
160
|
+
addEventListener(name, callback, options) {
|
|
161
|
+
dialogRef.current?.addEventListener(name, callback, options);
|
|
162
|
+
},
|
|
163
|
+
removeEventListener(name, callback, options) {
|
|
164
|
+
dialogRef.current?.removeEventListener(name, callback, options);
|
|
165
|
+
},
|
|
166
|
+
}), [isOpen, close, showModal]);
|
|
167
|
+
return (_jsx("dialog", { ref: dialogRef, onClose: onClose, ...props, className: `${type} ${size}${className ? ` ${className}` : ''}`, style: style, children: _jsx("div", { className: "dialog-content", children: children }) }));
|
|
168
|
+
});
|
|
169
|
+
Dialog.displayName = 'Dialog';
|
|
170
|
+
//# sourceMappingURL=Dialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Dialog.js","sourceRoot":"","sources":["../../../src/components/Modal/Dialog.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { DialogProps, DialogRef } from './Dialog.js';
|
|
2
|
+
/**
|
|
3
|
+
* Props for the Modal component.
|
|
4
|
+
*/
|
|
5
|
+
export type ModalProps = DialogProps & {
|
|
6
|
+
/**
|
|
7
|
+
* The title text displayed in the modal header.
|
|
8
|
+
*/
|
|
9
|
+
title?: string;
|
|
10
|
+
/**
|
|
11
|
+
* The subtitle text displayed below the title in the modal header.
|
|
12
|
+
*/
|
|
13
|
+
subtitle?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Optional identifier for the modal element.
|
|
16
|
+
*/
|
|
17
|
+
id?: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* A modal dialog component that displays content in an overlay with a header and close button.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* Using ref to control modal:
|
|
24
|
+
* ```tsx
|
|
25
|
+
* const modalRef = useRef<DialogRef>(null)
|
|
26
|
+
*
|
|
27
|
+
* const handleOpenModal = () => {
|
|
28
|
+
* modalRef.current?.showModal()
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* return (
|
|
32
|
+
* <>
|
|
33
|
+
* <button onClick={handleOpenModal}>Open Modal</button>
|
|
34
|
+
* <Modal
|
|
35
|
+
* ref={modalRef}
|
|
36
|
+
* title="Welcome"
|
|
37
|
+
* subtitle="Please review the information below"
|
|
38
|
+
* >
|
|
39
|
+
* <Text>Modal content goes here</Text>
|
|
40
|
+
* </Modal>
|
|
41
|
+
* </>
|
|
42
|
+
* )
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* Using controlled state:
|
|
47
|
+
* ```tsx
|
|
48
|
+
* const [isOpen, setIsOpen] = useState(false)
|
|
49
|
+
*
|
|
50
|
+
* return (
|
|
51
|
+
* <>
|
|
52
|
+
* <button onClick={() => setIsOpen(true)}>Open Modal</button>
|
|
53
|
+
* <Modal
|
|
54
|
+
* title="Confirmation"
|
|
55
|
+
* subtitle="Are you sure?"
|
|
56
|
+
* isOpen={isOpen}
|
|
57
|
+
* setIsOpen={setIsOpen}
|
|
58
|
+
* >
|
|
59
|
+
* <Text>Confirm your action</Text>
|
|
60
|
+
* </Modal>
|
|
61
|
+
* </>
|
|
62
|
+
* )
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* @param props - The modal properties
|
|
66
|
+
* @param ref - Forwarded ref to access Dialog methods
|
|
67
|
+
* @returns A modal dialog component
|
|
68
|
+
*/
|
|
69
|
+
export declare const Modal: import("react").ForwardRefExoticComponent<ModalProps & import("react").RefAttributes<DialogRef>>;
|
|
70
|
+
//# sourceMappingURL=Modal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Modal.d.ts","sourceRoot":"","sources":["../../../src/components/Modal/Modal.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAU,WAAW,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAEzD;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG;IACrC;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,eAAO,MAAM,KAAK,kGAiCjB,CAAA"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Column, IconBox, Row, Text } from '@stack-spot/citric-react';
|
|
3
|
+
import { forwardRef } from 'react';
|
|
4
|
+
import { Dialog } from './Dialog.js';
|
|
5
|
+
/**
|
|
6
|
+
* A modal dialog component that displays content in an overlay with a header and close button.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* Using ref to control modal:
|
|
10
|
+
* ```tsx
|
|
11
|
+
* const modalRef = useRef<DialogRef>(null)
|
|
12
|
+
*
|
|
13
|
+
* const handleOpenModal = () => {
|
|
14
|
+
* modalRef.current?.showModal()
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* return (
|
|
18
|
+
* <>
|
|
19
|
+
* <button onClick={handleOpenModal}>Open Modal</button>
|
|
20
|
+
* <Modal
|
|
21
|
+
* ref={modalRef}
|
|
22
|
+
* title="Welcome"
|
|
23
|
+
* subtitle="Please review the information below"
|
|
24
|
+
* >
|
|
25
|
+
* <Text>Modal content goes here</Text>
|
|
26
|
+
* </Modal>
|
|
27
|
+
* </>
|
|
28
|
+
* )
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* Using controlled state:
|
|
33
|
+
* ```tsx
|
|
34
|
+
* const [isOpen, setIsOpen] = useState(false)
|
|
35
|
+
*
|
|
36
|
+
* return (
|
|
37
|
+
* <>
|
|
38
|
+
* <button onClick={() => setIsOpen(true)}>Open Modal</button>
|
|
39
|
+
* <Modal
|
|
40
|
+
* title="Confirmation"
|
|
41
|
+
* subtitle="Are you sure?"
|
|
42
|
+
* isOpen={isOpen}
|
|
43
|
+
* setIsOpen={setIsOpen}
|
|
44
|
+
* >
|
|
45
|
+
* <Text>Confirm your action</Text>
|
|
46
|
+
* </Modal>
|
|
47
|
+
* </>
|
|
48
|
+
* )
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @param props - The modal properties
|
|
52
|
+
* @param ref - Forwarded ref to access Dialog methods
|
|
53
|
+
* @returns A modal dialog component
|
|
54
|
+
*/
|
|
55
|
+
export const Modal = forwardRef(({ title, subtitle, children, ...props }, ref) => {
|
|
56
|
+
const handleClose = () => {
|
|
57
|
+
if (ref && 'current' in ref && ref?.current) {
|
|
58
|
+
ref.current.close();
|
|
59
|
+
}
|
|
60
|
+
else if (props.setIsOpen) {
|
|
61
|
+
props.setIsOpen(false);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
return (_jsx(Dialog, { ref: ref, ...props, children: _jsxs(Column, { bg: "light.400", flex: 1, h: "100%", children: [_jsxs(Row, { justifyContent: "space-between", p: "20px", children: [_jsxs(Column, { children: [_jsx(Text, { appearance: "h4", children: title }), _jsx(Text, { color: "light.700", children: subtitle })] }), _jsx(IconBox, { onClick: handleClose, tag: "button", icon: "TimesMini", appearance: "circle", colorScheme: "light" })] }), _jsx(Column, { flex: 1, className: "content-box", p: "20px", children: children })] }) }));
|
|
65
|
+
});
|
|
66
|
+
Modal.displayName = 'Modal';
|
|
67
|
+
//# sourceMappingURL=Modal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Modal.js","sourceRoot":"","sources":["../../../src/components/Modal/Modal.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAA;AACrE,OAAO,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AAClC,OAAO,EAAE,MAAM,EAA0B,MAAM,UAAU,CAAA;AAsBzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,UAAU,CAC7B,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAE,EAAE,GAAG,EAAE,EAAE;IAC/C,MAAM,WAAW,GAAG,GAAG,EAAE;QACvB,IAAI,GAAG,IAAI,SAAS,IAAI,GAAG,IAAI,GAAG,EAAE,OAAO,EAAE,CAAC;YAC5C,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,CAAA;QACrB,CAAC;aAAM,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YAC3B,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;QACxB,CAAC;IACH,CAAC,CAAA;IAED,OAAO,CACL,KAAC,MAAM,IAAC,GAAG,EAAE,GAAG,KAAM,KAAK,YACzB,MAAC,MAAM,IAAC,EAAE,EAAC,WAAW,EAAC,IAAI,EAAE,CAAC,EAAE,CAAC,EAAC,MAAM,aACtC,MAAC,GAAG,IAAC,cAAc,EAAC,eAAe,EAAC,CAAC,EAAC,MAAM,aAC1C,MAAC,MAAM,eACL,KAAC,IAAI,IAAC,UAAU,EAAC,IAAI,YAAE,KAAK,GAAQ,EACpC,KAAC,IAAI,IAAC,KAAK,EAAC,WAAW,YAAE,QAAQ,GAAQ,IAClC,EACT,KAAC,OAAO,IACN,OAAO,EAAE,WAAW,EACpB,GAAG,EAAC,QAAQ,EACZ,IAAI,EAAC,WAAW,EAChB,UAAU,EAAC,QAAQ,EACnB,WAAW,EAAC,OAAO,GACnB,IACE,EACN,KAAC,MAAM,IAAC,IAAI,EAAE,CAAC,EAAE,SAAS,EAAC,aAAa,EAAC,CAAC,EAAC,MAAM,YAC9C,QAAQ,GACF,IACF,GACF,CACV,CAAA;AACH,CAAC,CACF,CAAA;AAED,KAAK,CAAC,WAAW,GAAG,OAAO,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Modal/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACjC,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAElE,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC/B,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/components/Modal/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAGjC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
|
|
2
|
+
/* Dialog */
|
|
3
|
+
dialog {
|
|
4
|
+
--animation-duration: 0.3s;
|
|
5
|
+
--animation-easing: ease-in-out;
|
|
6
|
+
padding: 0;
|
|
7
|
+
border: 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
dialog.small {
|
|
11
|
+
width: 400px;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
dialog.medium {
|
|
15
|
+
width: 600px;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
dialog.large {
|
|
19
|
+
width: 800px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
dialog.extra-large {
|
|
23
|
+
width: 70%;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
dialog>.dialog-content {
|
|
27
|
+
display: flex;
|
|
28
|
+
flex: 1;
|
|
29
|
+
width: 100%;
|
|
30
|
+
height: 100%;
|
|
31
|
+
|
|
32
|
+
.content-box {
|
|
33
|
+
overflow: auto;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Transition the :backdrop when the dialog modal is promoted to the top layer */
|
|
38
|
+
dialog::backdrop {
|
|
39
|
+
background-color: transparent;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
dialog:open::backdrop {
|
|
43
|
+
background-color: rgb(0 0 0 / 25%);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* This starting-style rule cannot be nested inside the above selector because the nesting selector cannot represent pseudo-elements. */
|
|
47
|
+
@starting-style {
|
|
48
|
+
dialog:open::backdrop {
|
|
49
|
+
background-color: transparent;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* MODAL */
|
|
54
|
+
|
|
55
|
+
/* Open state of the dialog */
|
|
56
|
+
dialog.modal:open {
|
|
57
|
+
opacity: 1;
|
|
58
|
+
transform: scale(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Closed state of the dialog */
|
|
62
|
+
dialog.modal {
|
|
63
|
+
opacity: 0;
|
|
64
|
+
transform: scale(0.5);
|
|
65
|
+
transition: all var(--animation-duration) allow-discrete;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* Before open state */
|
|
69
|
+
/* Needs to be after the previous dialog:open rule to take effect, as the specificity is the same */
|
|
70
|
+
@starting-style {
|
|
71
|
+
dialog.modal:open {
|
|
72
|
+
opacity: 0;
|
|
73
|
+
transform: scale(0.5);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Right Panel */
|
|
78
|
+
/* Closed state of the dialog */
|
|
79
|
+
dialog.right-panel {
|
|
80
|
+
height: 100vh;
|
|
81
|
+
left: auto;
|
|
82
|
+
max-height: 100vh;
|
|
83
|
+
scrollbar-gutter: stable;
|
|
84
|
+
/* Not widely available yet. */
|
|
85
|
+
|
|
86
|
+
opacity: 0;
|
|
87
|
+
translate: 100%;
|
|
88
|
+
transition: all var(--animation-duration) allow-discrete;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
dialog.right-panel:open {
|
|
92
|
+
opacity: 1;
|
|
93
|
+
translate: 0%;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Before open state */
|
|
97
|
+
/* Needs to be after the previous dialog:open rule to take effect, as the specificity is the same */
|
|
98
|
+
@starting-style {
|
|
99
|
+
dialog.right-panel:open {
|
|
100
|
+
opacity: 0;
|
|
101
|
+
translate: 100%;
|
|
102
|
+
}
|
|
103
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stack-spot/portal-components",
|
|
3
|
-
"version": "1.0.0-dev.
|
|
3
|
+
"version": "1.0.0-dev.1778512145216",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -28,16 +28,19 @@
|
|
|
28
28
|
"./FadingOverflow": "./dist/components/FadingOverflow.js",
|
|
29
29
|
"./Table": "./dist/components/Table/index.js",
|
|
30
30
|
"./ContentValidateFilter": "./dist/components/ContentValidateFilter.js",
|
|
31
|
-
"./Form": "./dist/components/form/Form/index.js"
|
|
31
|
+
"./Form": "./dist/components/form/Form/index.js",
|
|
32
|
+
"./Dialog": "./dist/components/Modal/Dialog.js",
|
|
33
|
+
"./Modal": "./dist/components/Modal/Modal.js"
|
|
32
34
|
},
|
|
33
35
|
"scripts": {
|
|
34
|
-
"build": "pnpm package-override --revert=false && rimraf dist && tsc && tsc-esm-fix --target='dist'",
|
|
36
|
+
"build": "pnpm package-override --revert=false && rimraf dist && tsc && tsc-esm-fix --target='dist' && cpy 'src/**/*.css' dist",
|
|
35
37
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
|
36
38
|
"check-tree-shaking": "agadoo"
|
|
37
39
|
},
|
|
38
40
|
"peerDependencies": {
|
|
39
41
|
"@citric/core": "^6.5.1",
|
|
40
42
|
"@citric/icons": "^5.10.0",
|
|
43
|
+
"@stack-spot/citric-react": "^0.41.2",
|
|
41
44
|
"@citric/ui": "^6.10.1",
|
|
42
45
|
"@stack-spot/portal-theme": "^1.1.1",
|
|
43
46
|
"@stack-spot/portal-translate": "^2.1.0",
|
|
@@ -51,16 +54,17 @@
|
|
|
51
54
|
"@types/react": "^18.2.37",
|
|
52
55
|
"@types/react-dom": "^18.2.15",
|
|
53
56
|
"@types/react-syntax-highlighter": "^15.5.13",
|
|
54
|
-
"@typescript-eslint/eslint-plugin": "^
|
|
55
|
-
"@typescript-eslint/parser": "^
|
|
57
|
+
"@typescript-eslint/eslint-plugin": "^8.25.0",
|
|
58
|
+
"@typescript-eslint/parser": "^8.25.0",
|
|
56
59
|
"agadoo": "^3.0.0",
|
|
57
|
-
"
|
|
60
|
+
"cpy-cli": "^5.0.0",
|
|
61
|
+
"eslint": "^9.26.0",
|
|
58
62
|
"eslint-plugin-filenames": "^1.3.2",
|
|
59
63
|
"eslint-plugin-import": "^2.29.0",
|
|
60
|
-
"eslint-plugin-lodash": "^
|
|
64
|
+
"eslint-plugin-lodash": "^8.0.0",
|
|
61
65
|
"eslint-plugin-promise": "^6.1.1",
|
|
62
66
|
"eslint-plugin-react": "^7.33.2",
|
|
63
|
-
"eslint-plugin-react-hooks": "^
|
|
67
|
+
"eslint-plugin-react-hooks": "^5.1.0",
|
|
64
68
|
"eslint-plugin-react-refresh": "^0.4.4",
|
|
65
69
|
"react": "18.2.0",
|
|
66
70
|
"react-dom": "18.2.0",
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AriaAttributes,
|
|
3
|
+
type CSSProperties,
|
|
4
|
+
type PropsWithChildren,
|
|
5
|
+
type SyntheticEvent,
|
|
6
|
+
forwardRef,
|
|
7
|
+
useEffect,
|
|
8
|
+
useImperativeHandle,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
} from 'react'
|
|
12
|
+
import './style.css'
|
|
13
|
+
/**
|
|
14
|
+
* Available size options for the dialog.
|
|
15
|
+
*/
|
|
16
|
+
export type DialogSize = 'small' | 'medium' | 'large' | 'extra-large' | 'fit-content'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Type of dialog display.
|
|
20
|
+
*
|
|
21
|
+
* @remarks
|
|
22
|
+
* - `modal`: Centered dialog with backdrop
|
|
23
|
+
* - `right-panel`: Slide-in panel from the right side
|
|
24
|
+
*/
|
|
25
|
+
type DialogType = 'modal' | 'right-panel'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Props for controlled dialog state.
|
|
29
|
+
*/
|
|
30
|
+
type ControlledProps = {
|
|
31
|
+
/**
|
|
32
|
+
* Controls whether the dialog is open.
|
|
33
|
+
*/
|
|
34
|
+
isOpen: boolean,
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Callback to update the dialog's open state.
|
|
38
|
+
*/
|
|
39
|
+
setIsOpen: (value: boolean) => void,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Props for uncontrolled dialog state.
|
|
44
|
+
*
|
|
45
|
+
* @remarks
|
|
46
|
+
* Use this when you want the dialog to manage its own state internally.
|
|
47
|
+
*/
|
|
48
|
+
type UncontrolledProps = {
|
|
49
|
+
isOpen?: never,
|
|
50
|
+
setIsOpen?: never,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type AriaProps = Pick<AriaAttributes, 'aria-labelledby' | 'aria-label' | 'aria-describedby' | 'aria-description'>
|
|
54
|
+
export type DialogProps = PropsWithChildren & (ControlledProps | UncontrolledProps) & AriaProps & {
|
|
55
|
+
/**
|
|
56
|
+
* The display type of the dialog.
|
|
57
|
+
*
|
|
58
|
+
* @defaultValue 'modal'
|
|
59
|
+
*/
|
|
60
|
+
type?: DialogType,
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Whether clicking outside the dialog should close it.
|
|
64
|
+
*
|
|
65
|
+
* @defaultValue true
|
|
66
|
+
*/
|
|
67
|
+
shouldLightDismiss?: boolean,
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Initial open state for uncontrolled dialogs.
|
|
71
|
+
*
|
|
72
|
+
* @defaultValue false
|
|
73
|
+
*/
|
|
74
|
+
initialOpen?: boolean,
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Callback fired when the dialog is closed.
|
|
78
|
+
*/
|
|
79
|
+
onClose?: (event?: SyntheticEvent) => void,
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The size of the dialog.
|
|
83
|
+
*
|
|
84
|
+
* @defaultValue 'medium'
|
|
85
|
+
*/
|
|
86
|
+
size?: DialogSize,
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Custom inline styles for the dialog element.
|
|
90
|
+
*/
|
|
91
|
+
style?: CSSProperties,
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Additional CSS class names.
|
|
95
|
+
*/
|
|
96
|
+
className?: string,
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Optional identifier for the dialog element.
|
|
100
|
+
*/
|
|
101
|
+
id?: string,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface DialogRef extends Pick<HTMLDialogElement, 'addEventListener' | 'removeEventListener'> {
|
|
105
|
+
close: () => void,
|
|
106
|
+
showModal: () => void,
|
|
107
|
+
isOpen: () => boolean,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Custom hook for managing dialog state and behavior.
|
|
112
|
+
*
|
|
113
|
+
* @remarks
|
|
114
|
+
* This hook handles:
|
|
115
|
+
* - Opening and closing the dialog
|
|
116
|
+
* - Light dismiss (clicking outside to close)
|
|
117
|
+
* - Escape key handling
|
|
118
|
+
* - Cleanup on unmount
|
|
119
|
+
*
|
|
120
|
+
* @param props - Dialog configuration options
|
|
121
|
+
* @returns Dialog state and control methods
|
|
122
|
+
*/
|
|
123
|
+
const useDialog = ({
|
|
124
|
+
isOpen: controlledOpen,
|
|
125
|
+
setIsOpen: setControlledOpen,
|
|
126
|
+
initialOpen = false,
|
|
127
|
+
shouldLightDismiss = true,
|
|
128
|
+
}: Pick<DialogProps, 'isOpen' | 'setIsOpen' | 'initialOpen' | 'shouldLightDismiss'>) => {
|
|
129
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(initialOpen)
|
|
130
|
+
const dialogRef = useRef<HTMLDialogElement>(null)
|
|
131
|
+
|
|
132
|
+
const isOpen = controlledOpen ?? uncontrolledOpen
|
|
133
|
+
const setIsOpen = setControlledOpen ?? setUncontrolledOpen
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
const dialog = dialogRef.current
|
|
137
|
+
if (isOpen && dialog && !dialog.open) {
|
|
138
|
+
dialog.showModal()
|
|
139
|
+
} else if (!isOpen) {
|
|
140
|
+
dialog?.close()
|
|
141
|
+
}
|
|
142
|
+
return () => {
|
|
143
|
+
dialog?.close()
|
|
144
|
+
}
|
|
145
|
+
}, [isOpen])
|
|
146
|
+
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
const dialog = dialogRef.current
|
|
149
|
+
if (!dialog) return
|
|
150
|
+
|
|
151
|
+
const handleClose = (event: Event | KeyboardEvent) => {
|
|
152
|
+
event.preventDefault()
|
|
153
|
+
event.stopPropagation()
|
|
154
|
+
setIsOpen(false)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const lightDismiss = (event: Event) => {
|
|
158
|
+
const { target } = event
|
|
159
|
+
if (target instanceof Element && target.nodeName === 'DIALOG') {
|
|
160
|
+
handleClose(event)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const closeOnEscape = (event: KeyboardEvent) => {
|
|
165
|
+
if (event.code === 'Escape') {
|
|
166
|
+
handleClose(event)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (shouldLightDismiss) {
|
|
171
|
+
dialog.addEventListener('click', lightDismiss)
|
|
172
|
+
}
|
|
173
|
+
dialog.addEventListener('keydown', closeOnEscape)
|
|
174
|
+
|
|
175
|
+
return () => {
|
|
176
|
+
if (shouldLightDismiss) {
|
|
177
|
+
dialog.removeEventListener('click', lightDismiss)
|
|
178
|
+
}
|
|
179
|
+
dialog.removeEventListener('keydown', closeOnEscape)
|
|
180
|
+
}
|
|
181
|
+
}, [shouldLightDismiss, setIsOpen])
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
dialogRef,
|
|
185
|
+
isOpen,
|
|
186
|
+
close: () => setIsOpen(false),
|
|
187
|
+
showModal: () => setIsOpen(true),
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* A flexible dialog component that supports both modal and panel modes.
|
|
193
|
+
*
|
|
194
|
+
* @remarks
|
|
195
|
+
* The Dialog component provides a native HTML dialog element with enhanced functionality:
|
|
196
|
+
* - **Controlled or uncontrolled state**: Manage state externally or let the component handle it
|
|
197
|
+
* - **Light dismiss**: Optional backdrop click to close
|
|
198
|
+
* - **Keyboard support**: ESC key closes the dialog
|
|
199
|
+
* - **Multiple sizes**: Predefined size options
|
|
200
|
+
* - **Accessibility**: Full ARIA support
|
|
201
|
+
* - **Two display modes**: Modal (centered) or right panel (slide-in)
|
|
202
|
+
*
|
|
203
|
+
* **Important:** When using controlled mode, you must provide both `isOpen` and `setIsOpen` props.
|
|
204
|
+
* For uncontrolled mode, use the ref to control the dialog programmatically.
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* Uncontrolled dialog with ref:
|
|
208
|
+
* ```tsx
|
|
209
|
+
* const dialogRef = useRef<DialogRef>(null)
|
|
210
|
+
*
|
|
211
|
+
* const handleOpen = () => {
|
|
212
|
+
* dialogRef.current?.showModal()
|
|
213
|
+
* }
|
|
214
|
+
*
|
|
215
|
+
* return (
|
|
216
|
+
* <>
|
|
217
|
+
* <button onClick={handleOpen}>Open Dialog</button>
|
|
218
|
+
* <Dialog
|
|
219
|
+
* ref={dialogRef}
|
|
220
|
+
* size="large"
|
|
221
|
+
* aria-label="Example dialog"
|
|
222
|
+
* >
|
|
223
|
+
* <Text>Dialog content</Text>
|
|
224
|
+
* </Dialog>
|
|
225
|
+
* </>
|
|
226
|
+
* )
|
|
227
|
+
* ```
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* Controlled dialog:
|
|
231
|
+
* ```tsx
|
|
232
|
+
* const [isOpen, setIsOpen] = useState(false)
|
|
233
|
+
*
|
|
234
|
+
* return (
|
|
235
|
+
* <>
|
|
236
|
+
* <button onClick={() => setIsOpen(true)}>Open Dialog</button>
|
|
237
|
+
* <Dialog
|
|
238
|
+
* isOpen={isOpen}
|
|
239
|
+
* setIsOpen={setIsOpen}
|
|
240
|
+
* type="right-panel"
|
|
241
|
+
* shouldLightDismiss={false}
|
|
242
|
+
* onClose={() => console.log('Dialog closed')}
|
|
243
|
+
* >
|
|
244
|
+
* <Text>Panel content</Text>
|
|
245
|
+
* </Dialog>
|
|
246
|
+
* </>
|
|
247
|
+
* )
|
|
248
|
+
* ```
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* Dialog with custom styling:
|
|
252
|
+
* ```tsx
|
|
253
|
+
* <Dialog
|
|
254
|
+
* size="fit-content"
|
|
255
|
+
* className="custom-dialog"
|
|
256
|
+
* style={{ maxWidth: '600px' }}
|
|
257
|
+
* aria-labelledby="dialog-title"
|
|
258
|
+
* >
|
|
259
|
+
* <h2 id="dialog-title">Custom Dialog</h2>
|
|
260
|
+
* <p>Content here</p>
|
|
261
|
+
* </Dialog>
|
|
262
|
+
* ```
|
|
263
|
+
*
|
|
264
|
+
* @param props - Dialog properties
|
|
265
|
+
* @param ref - Forwarded ref for imperative control
|
|
266
|
+
* @returns A dialog element
|
|
267
|
+
*/
|
|
268
|
+
export const Dialog = forwardRef<DialogRef, DialogProps>(
|
|
269
|
+
({
|
|
270
|
+
type = 'modal',
|
|
271
|
+
shouldLightDismiss = true,
|
|
272
|
+
initialOpen = false,
|
|
273
|
+
isOpen: controlledOpen,
|
|
274
|
+
setIsOpen: setControlledOpen,
|
|
275
|
+
onClose,
|
|
276
|
+
children,
|
|
277
|
+
size = 'medium',
|
|
278
|
+
style,
|
|
279
|
+
className,
|
|
280
|
+
...props
|
|
281
|
+
}, forwardedRef,
|
|
282
|
+
) => {
|
|
283
|
+
const { dialogRef, isOpen, close, showModal } = useDialog({
|
|
284
|
+
isOpen: controlledOpen,
|
|
285
|
+
setIsOpen: setControlledOpen,
|
|
286
|
+
initialOpen,
|
|
287
|
+
shouldLightDismiss,
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
useImperativeHandle(forwardedRef, () => ({
|
|
291
|
+
close,
|
|
292
|
+
showModal,
|
|
293
|
+
isOpen: () => isOpen,
|
|
294
|
+
addEventListener(name: string, callback: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions) {
|
|
295
|
+
dialogRef.current?.addEventListener(name, callback, options)
|
|
296
|
+
},
|
|
297
|
+
removeEventListener(name: string, callback: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions) {
|
|
298
|
+
dialogRef.current?.removeEventListener(name, callback, options)
|
|
299
|
+
},
|
|
300
|
+
}), [isOpen, close, showModal])
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<dialog
|
|
304
|
+
ref={dialogRef}
|
|
305
|
+
onClose={onClose}
|
|
306
|
+
{...props}
|
|
307
|
+
className={`${type} ${size}${className ? ` ${className}` : ''}`}
|
|
308
|
+
style={style}
|
|
309
|
+
>
|
|
310
|
+
<div className="dialog-content">
|
|
311
|
+
{children}
|
|
312
|
+
</div>
|
|
313
|
+
</dialog>
|
|
314
|
+
)
|
|
315
|
+
},
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
Dialog.displayName = 'Dialog'
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Column, IconBox, Row, Text } from '@stack-spot/citric-react'
|
|
2
|
+
import { forwardRef } from 'react'
|
|
3
|
+
import { Dialog, DialogProps, DialogRef } from './Dialog'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Props for the Modal component.
|
|
7
|
+
*/
|
|
8
|
+
export type ModalProps = DialogProps & {
|
|
9
|
+
/**
|
|
10
|
+
* The title text displayed in the modal header.
|
|
11
|
+
*/
|
|
12
|
+
title?: string,
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The subtitle text displayed below the title in the modal header.
|
|
16
|
+
*/
|
|
17
|
+
subtitle?: string,
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Optional identifier for the modal element.
|
|
21
|
+
*/
|
|
22
|
+
id?: string,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A modal dialog component that displays content in an overlay with a header and close button.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* Using ref to control modal:
|
|
30
|
+
* ```tsx
|
|
31
|
+
* const modalRef = useRef<DialogRef>(null)
|
|
32
|
+
*
|
|
33
|
+
* const handleOpenModal = () => {
|
|
34
|
+
* modalRef.current?.showModal()
|
|
35
|
+
* }
|
|
36
|
+
*
|
|
37
|
+
* return (
|
|
38
|
+
* <>
|
|
39
|
+
* <button onClick={handleOpenModal}>Open Modal</button>
|
|
40
|
+
* <Modal
|
|
41
|
+
* ref={modalRef}
|
|
42
|
+
* title="Welcome"
|
|
43
|
+
* subtitle="Please review the information below"
|
|
44
|
+
* >
|
|
45
|
+
* <Text>Modal content goes here</Text>
|
|
46
|
+
* </Modal>
|
|
47
|
+
* </>
|
|
48
|
+
* )
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* Using controlled state:
|
|
53
|
+
* ```tsx
|
|
54
|
+
* const [isOpen, setIsOpen] = useState(false)
|
|
55
|
+
*
|
|
56
|
+
* return (
|
|
57
|
+
* <>
|
|
58
|
+
* <button onClick={() => setIsOpen(true)}>Open Modal</button>
|
|
59
|
+
* <Modal
|
|
60
|
+
* title="Confirmation"
|
|
61
|
+
* subtitle="Are you sure?"
|
|
62
|
+
* isOpen={isOpen}
|
|
63
|
+
* setIsOpen={setIsOpen}
|
|
64
|
+
* >
|
|
65
|
+
* <Text>Confirm your action</Text>
|
|
66
|
+
* </Modal>
|
|
67
|
+
* </>
|
|
68
|
+
* )
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* @param props - The modal properties
|
|
72
|
+
* @param ref - Forwarded ref to access Dialog methods
|
|
73
|
+
* @returns A modal dialog component
|
|
74
|
+
*/
|
|
75
|
+
export const Modal = forwardRef<DialogRef, ModalProps>(
|
|
76
|
+
({ title, subtitle, children, ...props }, ref) => {
|
|
77
|
+
const handleClose = () => {
|
|
78
|
+
if (ref && 'current' in ref && ref?.current) {
|
|
79
|
+
ref.current.close()
|
|
80
|
+
} else if (props.setIsOpen) {
|
|
81
|
+
props.setIsOpen(false)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<Dialog ref={ref} {...props}>
|
|
87
|
+
<Column bg="light.400" flex={1} h="100%">
|
|
88
|
+
<Row justifyContent="space-between" p="20px">
|
|
89
|
+
<Column>
|
|
90
|
+
<Text appearance="h4">{title}</Text>
|
|
91
|
+
<Text color="light.700">{subtitle}</Text>
|
|
92
|
+
</Column>
|
|
93
|
+
<IconBox
|
|
94
|
+
onClick={handleClose}
|
|
95
|
+
tag="button"
|
|
96
|
+
icon="TimesMini"
|
|
97
|
+
appearance="circle"
|
|
98
|
+
colorScheme="light"
|
|
99
|
+
/>
|
|
100
|
+
</Row>
|
|
101
|
+
<Column flex={1} className="content-box" p="20px">
|
|
102
|
+
{children}
|
|
103
|
+
</Column>
|
|
104
|
+
</Column>
|
|
105
|
+
</Dialog>
|
|
106
|
+
)
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
Modal.displayName = 'Modal'
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
|
|
2
|
+
/* Dialog */
|
|
3
|
+
dialog {
|
|
4
|
+
--animation-duration: 0.3s;
|
|
5
|
+
--animation-easing: ease-in-out;
|
|
6
|
+
padding: 0;
|
|
7
|
+
border: 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
dialog.small {
|
|
11
|
+
width: 400px;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
dialog.medium {
|
|
15
|
+
width: 600px;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
dialog.large {
|
|
19
|
+
width: 800px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
dialog.extra-large {
|
|
23
|
+
width: 70%;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
dialog>.dialog-content {
|
|
27
|
+
display: flex;
|
|
28
|
+
flex: 1;
|
|
29
|
+
width: 100%;
|
|
30
|
+
height: 100%;
|
|
31
|
+
|
|
32
|
+
.content-box {
|
|
33
|
+
overflow: auto;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Transition the :backdrop when the dialog modal is promoted to the top layer */
|
|
38
|
+
dialog::backdrop {
|
|
39
|
+
background-color: transparent;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
dialog:open::backdrop {
|
|
43
|
+
background-color: rgb(0 0 0 / 25%);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* This starting-style rule cannot be nested inside the above selector because the nesting selector cannot represent pseudo-elements. */
|
|
47
|
+
@starting-style {
|
|
48
|
+
dialog:open::backdrop {
|
|
49
|
+
background-color: transparent;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* MODAL */
|
|
54
|
+
|
|
55
|
+
/* Open state of the dialog */
|
|
56
|
+
dialog.modal:open {
|
|
57
|
+
opacity: 1;
|
|
58
|
+
transform: scale(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Closed state of the dialog */
|
|
62
|
+
dialog.modal {
|
|
63
|
+
opacity: 0;
|
|
64
|
+
transform: scale(0.5);
|
|
65
|
+
transition: all var(--animation-duration) allow-discrete;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* Before open state */
|
|
69
|
+
/* Needs to be after the previous dialog:open rule to take effect, as the specificity is the same */
|
|
70
|
+
@starting-style {
|
|
71
|
+
dialog.modal:open {
|
|
72
|
+
opacity: 0;
|
|
73
|
+
transform: scale(0.5);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Right Panel */
|
|
78
|
+
/* Closed state of the dialog */
|
|
79
|
+
dialog.right-panel {
|
|
80
|
+
height: 100vh;
|
|
81
|
+
left: auto;
|
|
82
|
+
max-height: 100vh;
|
|
83
|
+
scrollbar-gutter: stable;
|
|
84
|
+
/* Not widely available yet. */
|
|
85
|
+
|
|
86
|
+
opacity: 0;
|
|
87
|
+
translate: 100%;
|
|
88
|
+
transition: all var(--animation-duration) allow-discrete;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
dialog.right-panel:open {
|
|
92
|
+
opacity: 1;
|
|
93
|
+
translate: 0%;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Before open state */
|
|
97
|
+
/* Needs to be after the previous dialog:open rule to take effect, as the specificity is the same */
|
|
98
|
+
@starting-style {
|
|
99
|
+
dialog.right-panel:open {
|
|
100
|
+
opacity: 0;
|
|
101
|
+
translate: 100%;
|
|
102
|
+
}
|
|
103
|
+
}
|