universal-modal-monorepo 1.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/AUTHORS +5 -0
- package/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/NOTICE +6 -0
- package/README.md +239 -0
- package/SECURITY.md +21 -0
- package/docs/ACCESSIBILITY.md +43 -0
- package/docs/API.md +56 -0
- package/docs/THEMING.md +65 -0
- package/examples/angular/app.component.html +137 -0
- package/examples/angular/app.component.ts +53 -0
- package/examples/angular/index.html +99 -0
- package/examples/angular/main.js +121 -0
- package/examples/angular/styles.css +25 -0
- package/examples/index.html +53 -0
- package/examples/react/App.tsx +242 -0
- package/examples/react/index.html +26 -0
- package/examples/react/main.js +144 -0
- package/examples/react/styles.css +1 -0
- package/examples/shared/gallery.css +177 -0
- package/examples/vanilla/index.html +89 -0
- package/examples/vanilla/main.js +174 -0
- package/examples/vanilla/styles.css +1 -0
- package/examples/vue/App.vue +190 -0
- package/examples/vue/index.html +23 -0
- package/examples/vue/main.js +174 -0
- package/examples/vue/styles.css +1 -0
- package/examples/web-component/index.html +190 -0
- package/examples/web-component/main.js +58 -0
- package/examples/web-component/styles.css +11 -0
- package/package.json +36 -0
- package/packages/universal-modal/AUTHORS +5 -0
- package/packages/universal-modal/CHANGELOG.md +10 -0
- package/packages/universal-modal/LICENSE +21 -0
- package/packages/universal-modal/NOTICE +6 -0
- package/packages/universal-modal/README.md +29 -0
- package/packages/universal-modal/package.json +83 -0
- package/packages/universal-modal/scripts/copy-styles.mjs +11 -0
- package/packages/universal-modal/src/core/constants.ts +50 -0
- package/packages/universal-modal/src/core/event-emitter.ts +46 -0
- package/packages/universal-modal/src/core/focus-trap.ts +101 -0
- package/packages/universal-modal/src/core/index.ts +39 -0
- package/packages/universal-modal/src/core/modal-engine.ts +410 -0
- package/packages/universal-modal/src/core/modal-stack.ts +68 -0
- package/packages/universal-modal/src/core/scroll-lock.ts +78 -0
- package/packages/universal-modal/src/core/theme.ts +50 -0
- package/packages/universal-modal/src/core/types.ts +196 -0
- package/packages/universal-modal/src/core/utils.ts +114 -0
- package/packages/universal-modal/src/dom/index.ts +1 -0
- package/packages/universal-modal/src/dom/modal-renderer.ts +191 -0
- package/packages/universal-modal/src/index.ts +2 -0
- package/packages/universal-modal/src/styles/modal.css +368 -0
- package/packages/universal-modal/tsconfig.json +24 -0
- package/packages/universal-modal/tsup.config.ts +12 -0
- package/packages/universal-modal-angular/AUTHORS +5 -0
- package/packages/universal-modal-angular/CHANGELOG.md +9 -0
- package/packages/universal-modal-angular/LICENSE +21 -0
- package/packages/universal-modal-angular/NOTICE +6 -0
- package/packages/universal-modal-angular/README.md +12 -0
- package/packages/universal-modal-angular/package.json +85 -0
- package/packages/universal-modal-angular/src/index.ts +10 -0
- package/packages/universal-modal-angular/src/universal-modal.component.ts +275 -0
- package/packages/universal-modal-angular/tsconfig.json +20 -0
- package/packages/universal-modal-angular/tsup.config.ts +10 -0
- package/packages/universal-modal-react/AUTHORS +5 -0
- package/packages/universal-modal-react/CHANGELOG.md +9 -0
- package/packages/universal-modal-react/LICENSE +21 -0
- package/packages/universal-modal-react/NOTICE +6 -0
- package/packages/universal-modal-react/README.md +12 -0
- package/packages/universal-modal-react/package.json +87 -0
- package/packages/universal-modal-react/src/UniversalModal.tsx +340 -0
- package/packages/universal-modal-react/src/index.ts +10 -0
- package/packages/universal-modal-react/tsconfig.json +19 -0
- package/packages/universal-modal-react/tsup.config.ts +13 -0
- package/packages/universal-modal-vanilla/AUTHORS +5 -0
- package/packages/universal-modal-vanilla/CHANGELOG.md +9 -0
- package/packages/universal-modal-vanilla/LICENSE +21 -0
- package/packages/universal-modal-vanilla/NOTICE +6 -0
- package/packages/universal-modal-vanilla/README.md +12 -0
- package/packages/universal-modal-vanilla/package.json +79 -0
- package/packages/universal-modal-vanilla/src/create-modal.ts +125 -0
- package/packages/universal-modal-vanilla/src/index.ts +14 -0
- package/packages/universal-modal-vanilla/tsconfig.json +18 -0
- package/packages/universal-modal-vanilla/tsup.config.ts +10 -0
- package/packages/universal-modal-vue/AUTHORS +5 -0
- package/packages/universal-modal-vue/CHANGELOG.md +9 -0
- package/packages/universal-modal-vue/LICENSE +21 -0
- package/packages/universal-modal-vue/NOTICE +6 -0
- package/packages/universal-modal-vue/README.md +12 -0
- package/packages/universal-modal-vue/package.json +83 -0
- package/packages/universal-modal-vue/src/UniversalModal.ts +342 -0
- package/packages/universal-modal-vue/src/index.ts +10 -0
- package/packages/universal-modal-vue/tsconfig.json +18 -0
- package/packages/universal-modal-vue/tsup.config.ts +10 -0
- package/packages/universal-modal-web-component/AUTHORS +5 -0
- package/packages/universal-modal-web-component/CHANGELOG.md +9 -0
- package/packages/universal-modal-web-component/LICENSE +21 -0
- package/packages/universal-modal-web-component/NOTICE +6 -0
- package/packages/universal-modal-web-component/README.md +12 -0
- package/packages/universal-modal-web-component/package.json +83 -0
- package/packages/universal-modal-web-component/src/index.ts +11 -0
- package/packages/universal-modal-web-component/src/universal-modal-element.ts +424 -0
- package/packages/universal-modal-web-component/tsconfig.json +18 -0
- package/packages/universal-modal-web-component/tsup.config.ts +10 -0
- package/scripts/copy-styles.mjs +11 -0
- package/scripts/serve-examples.mjs +48 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/** Modal size tokens. */
|
|
2
|
+
export type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'fullscreen';
|
|
3
|
+
|
|
4
|
+
/** Modal placement relative to the viewport. */
|
|
5
|
+
export type ModalPlacement = 'center' | 'top' | 'bottom' | 'left' | 'right';
|
|
6
|
+
|
|
7
|
+
/** Why the modal closed. */
|
|
8
|
+
export type CloseReason =
|
|
9
|
+
| 'esc'
|
|
10
|
+
| 'backdrop'
|
|
11
|
+
| 'close-icon'
|
|
12
|
+
| 'cancel'
|
|
13
|
+
| 'confirm'
|
|
14
|
+
| 'programmatic';
|
|
15
|
+
|
|
16
|
+
/** Dialog ARIA role. Use `alertdialog` for confirmations that interrupt flow. */
|
|
17
|
+
export type ModalRole = 'dialog' | 'alertdialog';
|
|
18
|
+
|
|
19
|
+
/** Content that can be a string, node, or lazy factory. */
|
|
20
|
+
export type ModalContent =
|
|
21
|
+
| string
|
|
22
|
+
| Node
|
|
23
|
+
| (() => string | Node | null | undefined);
|
|
24
|
+
|
|
25
|
+
export type LifecycleResult = boolean | void | Promise<boolean | void>;
|
|
26
|
+
|
|
27
|
+
export interface ModalLifecycleHooks {
|
|
28
|
+
beforeOpen?: () => LifecycleResult;
|
|
29
|
+
afterOpen?: () => void;
|
|
30
|
+
beforeClose?: (reason: CloseReason) => LifecycleResult;
|
|
31
|
+
afterClose?: (reason: CloseReason) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ModalActionHandlers {
|
|
35
|
+
onConfirm?: () => void | Promise<void>;
|
|
36
|
+
onCancel?: () => void | Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ModalA11yOptions {
|
|
40
|
+
role?: ModalRole;
|
|
41
|
+
/** Accessible name when no visible title / labelledby is available. */
|
|
42
|
+
ariaLabel?: string;
|
|
43
|
+
ariaLabelledBy?: string;
|
|
44
|
+
ariaDescribedBy?: string;
|
|
45
|
+
/** Selector, element, or factory for the element to focus on open. */
|
|
46
|
+
initialFocus?: string | HTMLElement | (() => HTMLElement | null);
|
|
47
|
+
/** Element that opened the modal; focus is restored here on close. */
|
|
48
|
+
returnFocus?: HTMLElement | (() => HTMLElement | null) | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ModalBehaviorOptions {
|
|
52
|
+
/** Prevent ESC / backdrop / outside dismiss. Close icon & programmatic still work unless disabled. */
|
|
53
|
+
persistent?: boolean;
|
|
54
|
+
closeOnEsc?: boolean;
|
|
55
|
+
closeOnBackdrop?: boolean;
|
|
56
|
+
showCloseIcon?: boolean;
|
|
57
|
+
/** Base z-index; stacking adds per-layer offsets. */
|
|
58
|
+
zIndexBase?: number;
|
|
59
|
+
animationDuration?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ModalContentOptions {
|
|
63
|
+
size?: ModalSize;
|
|
64
|
+
placement?: ModalPlacement;
|
|
65
|
+
title?: string;
|
|
66
|
+
description?: string;
|
|
67
|
+
header?: ModalContent;
|
|
68
|
+
body?: ModalContent;
|
|
69
|
+
footer?: ModalContent;
|
|
70
|
+
confirmText?: string;
|
|
71
|
+
cancelText?: string;
|
|
72
|
+
/** Hide built-in confirm/cancel footer actions. */
|
|
73
|
+
hideActions?: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Runtime theme overrides mapped to CSS custom properties on the modal root.
|
|
78
|
+
* Prefer CSS variables globally; use `theme` for per-instance branding.
|
|
79
|
+
*/
|
|
80
|
+
export interface ModalTheme {
|
|
81
|
+
/** Primary brand color (buttons, focus ring). Maps to `--um-color-brand`. */
|
|
82
|
+
brand?: string;
|
|
83
|
+
/** Text/icon color on brand surfaces. Maps to `--um-color-brand-fg`. */
|
|
84
|
+
brandForeground?: string;
|
|
85
|
+
confirmBg?: string;
|
|
86
|
+
confirmFg?: string;
|
|
87
|
+
confirmBorder?: string;
|
|
88
|
+
cancelBg?: string;
|
|
89
|
+
cancelFg?: string;
|
|
90
|
+
cancelBorder?: string;
|
|
91
|
+
focusRing?: string;
|
|
92
|
+
/** Escape hatch: any `--um-*` custom property (with or without `--` prefix). */
|
|
93
|
+
cssVars?: Record<string, string>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ModalOptions
|
|
97
|
+
extends ModalLifecycleHooks,
|
|
98
|
+
ModalActionHandlers,
|
|
99
|
+
ModalA11yOptions,
|
|
100
|
+
ModalBehaviorOptions,
|
|
101
|
+
ModalContentOptions {
|
|
102
|
+
id?: string;
|
|
103
|
+
className?: string;
|
|
104
|
+
/** Per-instance brand / button color overrides. */
|
|
105
|
+
theme?: ModalTheme;
|
|
106
|
+
/** Mount node; defaults to `document.body`. */
|
|
107
|
+
container?: HTMLElement | null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type ModalEventMap = {
|
|
111
|
+
beforeOpen: undefined;
|
|
112
|
+
afterOpen: undefined;
|
|
113
|
+
beforeClose: CloseReason;
|
|
114
|
+
afterClose: CloseReason;
|
|
115
|
+
confirm: undefined;
|
|
116
|
+
cancel: undefined;
|
|
117
|
+
loadingChange: boolean;
|
|
118
|
+
openChange: boolean;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export type ModalEventName = keyof ModalEventMap;
|
|
122
|
+
|
|
123
|
+
export type ModalListener<T> = T extends undefined
|
|
124
|
+
? () => void
|
|
125
|
+
: (payload: T) => void;
|
|
126
|
+
|
|
127
|
+
export interface ModalState {
|
|
128
|
+
id: string;
|
|
129
|
+
open: boolean;
|
|
130
|
+
loading: boolean;
|
|
131
|
+
size: ModalSize;
|
|
132
|
+
placement: ModalPlacement;
|
|
133
|
+
persistent: boolean;
|
|
134
|
+
closeOnEsc: boolean;
|
|
135
|
+
closeOnBackdrop: boolean;
|
|
136
|
+
showCloseIcon: boolean;
|
|
137
|
+
role: ModalRole;
|
|
138
|
+
zIndex: number;
|
|
139
|
+
title?: string;
|
|
140
|
+
description?: string;
|
|
141
|
+
confirmText: string;
|
|
142
|
+
cancelText: string;
|
|
143
|
+
hideActions: boolean;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface ModalHostElements {
|
|
147
|
+
root: HTMLElement;
|
|
148
|
+
backdrop: HTMLElement;
|
|
149
|
+
dialog: HTMLElement;
|
|
150
|
+
panel: HTMLElement;
|
|
151
|
+
header?: HTMLElement | null;
|
|
152
|
+
body?: HTMLElement | null;
|
|
153
|
+
footer?: HTMLElement | null;
|
|
154
|
+
closeButton?: HTMLElement | null;
|
|
155
|
+
confirmButton?: HTMLButtonElement | null;
|
|
156
|
+
cancelButton?: HTMLButtonElement | null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface ResolvedModalOptions extends Required<
|
|
160
|
+
Pick<
|
|
161
|
+
ModalOptions,
|
|
162
|
+
| 'size'
|
|
163
|
+
| 'placement'
|
|
164
|
+
| 'persistent'
|
|
165
|
+
| 'closeOnEsc'
|
|
166
|
+
| 'closeOnBackdrop'
|
|
167
|
+
| 'showCloseIcon'
|
|
168
|
+
| 'role'
|
|
169
|
+
| 'zIndexBase'
|
|
170
|
+
| 'animationDuration'
|
|
171
|
+
| 'confirmText'
|
|
172
|
+
| 'cancelText'
|
|
173
|
+
| 'hideActions'
|
|
174
|
+
>
|
|
175
|
+
> {
|
|
176
|
+
id: string;
|
|
177
|
+
title?: string;
|
|
178
|
+
description?: string;
|
|
179
|
+
ariaLabel?: string;
|
|
180
|
+
ariaLabelledBy?: string;
|
|
181
|
+
ariaDescribedBy?: string;
|
|
182
|
+
className?: string;
|
|
183
|
+
theme?: ModalTheme;
|
|
184
|
+
header?: ModalContent;
|
|
185
|
+
body?: ModalContent;
|
|
186
|
+
footer?: ModalContent;
|
|
187
|
+
container: HTMLElement | null;
|
|
188
|
+
initialFocus?: ModalA11yOptions['initialFocus'];
|
|
189
|
+
returnFocus?: ModalA11yOptions['returnFocus'];
|
|
190
|
+
beforeOpen?: ModalLifecycleHooks['beforeOpen'];
|
|
191
|
+
afterOpen?: ModalLifecycleHooks['afterOpen'];
|
|
192
|
+
beforeClose?: ModalLifecycleHooks['beforeClose'];
|
|
193
|
+
afterClose?: ModalLifecycleHooks['afterClose'];
|
|
194
|
+
onConfirm?: ModalActionHandlers['onConfirm'];
|
|
195
|
+
onCancel?: ModalActionHandlers['onCancel'];
|
|
196
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { DEFAULT_OPTIONS } from './constants';
|
|
2
|
+
import type { ModalContent, ModalOptions, ResolvedModalOptions } from './types';
|
|
3
|
+
|
|
4
|
+
let idCounter = 0;
|
|
5
|
+
|
|
6
|
+
export function createModalId(prefix = 'um-modal'): string {
|
|
7
|
+
idCounter += 1;
|
|
8
|
+
return `${prefix}-${idCounter}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function omitUndefined<T extends Record<string, unknown>>(input: T): Partial<T> {
|
|
12
|
+
const result: Partial<T> = {};
|
|
13
|
+
for (const [key, value] of Object.entries(input)) {
|
|
14
|
+
if (value !== undefined) {
|
|
15
|
+
(result as Record<string, unknown>)[key] = value;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function resolveOptions(options: ModalOptions = {}): ResolvedModalOptions {
|
|
22
|
+
const id = options.id ?? createModalId();
|
|
23
|
+
const cleaned = omitUndefined(options as Record<string, unknown>) as ModalOptions;
|
|
24
|
+
const persistent = cleaned.persistent ?? DEFAULT_OPTIONS.persistent;
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
...DEFAULT_OPTIONS,
|
|
28
|
+
...cleaned,
|
|
29
|
+
id,
|
|
30
|
+
persistent,
|
|
31
|
+
container: cleaned.container ?? (typeof document !== 'undefined' ? document.body : null),
|
|
32
|
+
closeOnEsc: persistent ? false : (cleaned.closeOnEsc ?? DEFAULT_OPTIONS.closeOnEsc),
|
|
33
|
+
closeOnBackdrop: persistent
|
|
34
|
+
? false
|
|
35
|
+
: (cleaned.closeOnBackdrop ?? DEFAULT_OPTIONS.closeOnBackdrop),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function resolveContent(content: ModalContent | undefined): string | Node | null {
|
|
40
|
+
if (content == null) return null;
|
|
41
|
+
if (typeof content === 'function') {
|
|
42
|
+
const result = content();
|
|
43
|
+
return result ?? null;
|
|
44
|
+
}
|
|
45
|
+
return content;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function setContent(target: HTMLElement, content: ModalContent | undefined): void {
|
|
49
|
+
target.replaceChildren();
|
|
50
|
+
const resolved = resolveContent(content);
|
|
51
|
+
if (resolved == null) return;
|
|
52
|
+
if (typeof resolved === 'string') {
|
|
53
|
+
target.innerHTML = resolved;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
target.appendChild(resolved);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function isPromiseLike<T>(value: unknown): value is PromiseLike<T> {
|
|
60
|
+
return (
|
|
61
|
+
typeof value === 'object' &&
|
|
62
|
+
value !== null &&
|
|
63
|
+
'then' in value &&
|
|
64
|
+
typeof (value as PromiseLike<T>).then === 'function'
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function runLifecycle(
|
|
69
|
+
hook: (() => unknown) | ((arg: never) => unknown) | undefined,
|
|
70
|
+
arg?: unknown,
|
|
71
|
+
): Promise<boolean> {
|
|
72
|
+
if (!hook) return true;
|
|
73
|
+
const result = arg === undefined ? (hook as () => unknown)() : (hook as (a: unknown) => unknown)(arg);
|
|
74
|
+
const value = isPromiseLike(result) ? await result : result;
|
|
75
|
+
return value !== false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getReturnFocusElement(
|
|
79
|
+
returnFocus: ModalOptions['returnFocus'],
|
|
80
|
+
fallback: Element | null = null,
|
|
81
|
+
): HTMLElement | null {
|
|
82
|
+
if (returnFocus === null) return null;
|
|
83
|
+
if (typeof returnFocus === 'function') {
|
|
84
|
+
return returnFocus() ?? (fallback instanceof HTMLElement ? fallback : null);
|
|
85
|
+
}
|
|
86
|
+
if (returnFocus instanceof HTMLElement) return returnFocus;
|
|
87
|
+
if (fallback instanceof HTMLElement) return fallback;
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function resolveInitialFocus(
|
|
92
|
+
initialFocus: ModalOptions['initialFocus'],
|
|
93
|
+
dialog: HTMLElement,
|
|
94
|
+
): HTMLElement | null {
|
|
95
|
+
if (!initialFocus) {
|
|
96
|
+
const autofocus = dialog.querySelector<HTMLElement>('[autofocus]');
|
|
97
|
+
if (autofocus) return autofocus;
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
if (typeof initialFocus === 'string') {
|
|
101
|
+
return dialog.querySelector<HTMLElement>(initialFocus);
|
|
102
|
+
}
|
|
103
|
+
if (typeof initialFocus === 'function') {
|
|
104
|
+
return initialFocus();
|
|
105
|
+
}
|
|
106
|
+
return initialFocus;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function prefersReducedMotion(): boolean {
|
|
110
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
114
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ModalRenderer, type ModalRendererOptions } from './modal-renderer';
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { CSS_CLASSES } from '../core/constants';
|
|
2
|
+
import type { ModalEngine } from '../core/modal-engine';
|
|
3
|
+
import type { ModalHostElements, ModalOptions } from '../core/types';
|
|
4
|
+
import { setContent } from '../core/utils';
|
|
5
|
+
|
|
6
|
+
export interface ModalRendererOptions {
|
|
7
|
+
/** Include confirm/cancel action buttons when options allow. */
|
|
8
|
+
includeActions?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface InternalElements extends ModalHostElements {
|
|
12
|
+
titleEl: HTMLHeadingElement;
|
|
13
|
+
descriptionEl: HTMLParagraphElement;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Imperative DOM builder used by Vanilla JS and Web Component adapters.
|
|
18
|
+
*/
|
|
19
|
+
export class ModalRenderer {
|
|
20
|
+
readonly elements: ModalHostElements;
|
|
21
|
+
private readonly engine: ModalEngine;
|
|
22
|
+
private readonly titleEl: HTMLHeadingElement;
|
|
23
|
+
private readonly descriptionEl: HTMLParagraphElement;
|
|
24
|
+
|
|
25
|
+
constructor(engine: ModalEngine, _options: ModalRendererOptions = {}) {
|
|
26
|
+
this.engine = engine;
|
|
27
|
+
const built = this.build();
|
|
28
|
+
this.elements = built;
|
|
29
|
+
this.titleEl = built.titleEl;
|
|
30
|
+
this.descriptionEl = built.descriptionEl;
|
|
31
|
+
this.syncContent();
|
|
32
|
+
this.engine.attach(this.elements);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
mount(container?: HTMLElement | null): void {
|
|
36
|
+
const target = container ?? this.engine.getOptions().container ?? document.body;
|
|
37
|
+
if (!this.elements.root.isConnected) {
|
|
38
|
+
target.appendChild(this.elements.root);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
unmount(): void {
|
|
43
|
+
this.elements.root.remove();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
syncContent(options: Partial<ModalOptions> = {}): void {
|
|
47
|
+
if (Object.keys(options).length > 0) {
|
|
48
|
+
this.engine.updateOptions(options);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const opts = this.engine.getOptions();
|
|
52
|
+
const { header, body, footer } = this.elements;
|
|
53
|
+
const id = opts.id;
|
|
54
|
+
|
|
55
|
+
if (header) {
|
|
56
|
+
header.replaceChildren();
|
|
57
|
+
|
|
58
|
+
if (opts.header) {
|
|
59
|
+
setContent(header, opts.header);
|
|
60
|
+
} else {
|
|
61
|
+
if (opts.title) {
|
|
62
|
+
this.titleEl.id = `${id}-title`;
|
|
63
|
+
this.titleEl.textContent = opts.title;
|
|
64
|
+
header.appendChild(this.titleEl);
|
|
65
|
+
}
|
|
66
|
+
if (opts.description) {
|
|
67
|
+
this.descriptionEl.id = `${id}-description`;
|
|
68
|
+
this.descriptionEl.textContent = opts.description;
|
|
69
|
+
header.appendChild(this.descriptionEl);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
header.hidden =
|
|
74
|
+
header.childElementCount === 0 && !opts.title && !opts.description && !opts.header;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (body) {
|
|
78
|
+
if (opts.body !== undefined) {
|
|
79
|
+
setContent(body, opts.body);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (footer) {
|
|
84
|
+
footer.replaceChildren();
|
|
85
|
+
|
|
86
|
+
if (opts.footer) {
|
|
87
|
+
setContent(footer, opts.footer);
|
|
88
|
+
} else if (!opts.hideActions) {
|
|
89
|
+
const actions = document.createElement('div');
|
|
90
|
+
actions.className = CSS_CLASSES.actions;
|
|
91
|
+
|
|
92
|
+
if (this.elements.cancelButton) {
|
|
93
|
+
this.elements.cancelButton.textContent = opts.cancelText;
|
|
94
|
+
actions.appendChild(this.elements.cancelButton);
|
|
95
|
+
}
|
|
96
|
+
if (this.elements.confirmButton) {
|
|
97
|
+
this.elements.confirmButton.textContent = opts.confirmText;
|
|
98
|
+
if (!this.elements.confirmButton.querySelector(`.${CSS_CLASSES.spinner}`)) {
|
|
99
|
+
const spin = document.createElement('span');
|
|
100
|
+
spin.className = CSS_CLASSES.spinner;
|
|
101
|
+
spin.setAttribute('aria-hidden', 'true');
|
|
102
|
+
this.elements.confirmButton.appendChild(spin);
|
|
103
|
+
}
|
|
104
|
+
actions.appendChild(this.elements.confirmButton);
|
|
105
|
+
}
|
|
106
|
+
footer.appendChild(actions);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
footer.hidden = footer.childElementCount === 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (this.elements.closeButton) {
|
|
113
|
+
this.elements.closeButton.hidden = !opts.showCloseIcon;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
destroy(): void {
|
|
118
|
+
this.engine.detach();
|
|
119
|
+
this.unmount();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private build(): InternalElements {
|
|
123
|
+
const opts = this.engine.getOptions();
|
|
124
|
+
|
|
125
|
+
const root = document.createElement('div');
|
|
126
|
+
root.className = CSS_CLASSES.root;
|
|
127
|
+
root.hidden = true;
|
|
128
|
+
root.setAttribute('aria-hidden', 'true');
|
|
129
|
+
|
|
130
|
+
const backdrop = document.createElement('div');
|
|
131
|
+
backdrop.className = CSS_CLASSES.backdrop;
|
|
132
|
+
backdrop.setAttribute('data-um-backdrop', '');
|
|
133
|
+
|
|
134
|
+
const dialog = document.createElement('div');
|
|
135
|
+
dialog.className = CSS_CLASSES.dialog;
|
|
136
|
+
|
|
137
|
+
const panel = document.createElement('div');
|
|
138
|
+
panel.className = CSS_CLASSES.panel;
|
|
139
|
+
panel.dataset.size = opts.size;
|
|
140
|
+
panel.dataset.placement = opts.placement;
|
|
141
|
+
|
|
142
|
+
const closeButton = document.createElement('button');
|
|
143
|
+
closeButton.type = 'button';
|
|
144
|
+
closeButton.className = CSS_CLASSES.close;
|
|
145
|
+
closeButton.setAttribute('aria-label', 'Close dialog');
|
|
146
|
+
closeButton.innerHTML =
|
|
147
|
+
'<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" focusable="false"><path d="M6.4 6.4l11.2 11.2M17.6 6.4L6.4 17.6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>';
|
|
148
|
+
|
|
149
|
+
const header = document.createElement('div');
|
|
150
|
+
header.className = CSS_CLASSES.header;
|
|
151
|
+
|
|
152
|
+
const titleEl = document.createElement('h2');
|
|
153
|
+
titleEl.className = CSS_CLASSES.title;
|
|
154
|
+
|
|
155
|
+
const descriptionEl = document.createElement('p');
|
|
156
|
+
descriptionEl.className = CSS_CLASSES.description;
|
|
157
|
+
|
|
158
|
+
const body = document.createElement('div');
|
|
159
|
+
body.className = CSS_CLASSES.body;
|
|
160
|
+
|
|
161
|
+
const footer = document.createElement('div');
|
|
162
|
+
footer.className = CSS_CLASSES.footer;
|
|
163
|
+
|
|
164
|
+
const cancelButton = document.createElement('button');
|
|
165
|
+
cancelButton.type = 'button';
|
|
166
|
+
cancelButton.className = CSS_CLASSES.cancel;
|
|
167
|
+
|
|
168
|
+
const confirmButton = document.createElement('button');
|
|
169
|
+
confirmButton.type = 'button';
|
|
170
|
+
confirmButton.className = CSS_CLASSES.confirm;
|
|
171
|
+
|
|
172
|
+
panel.append(closeButton, header, body, footer);
|
|
173
|
+
dialog.appendChild(panel);
|
|
174
|
+
root.append(backdrop, dialog);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
root,
|
|
178
|
+
backdrop,
|
|
179
|
+
dialog,
|
|
180
|
+
panel,
|
|
181
|
+
header,
|
|
182
|
+
body,
|
|
183
|
+
footer,
|
|
184
|
+
closeButton,
|
|
185
|
+
confirmButton,
|
|
186
|
+
cancelButton,
|
|
187
|
+
titleEl,
|
|
188
|
+
descriptionEl,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|