vanilla-jui 1.3.11 → 1.4.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.
Files changed (106) hide show
  1. package/dist/index.d.ts +2921 -0
  2. package/dist/index.js +1 -1
  3. package/dist/index.umd.js +1 -1
  4. package/dist/style.css +1 -1
  5. package/package.json +4 -3
  6. package/src/components/accordion.ts +522 -0
  7. package/src/components/drop.ts +497 -0
  8. package/src/components/flow.ts +1579 -0
  9. package/src/components/form.ts +953 -0
  10. package/src/components/{icons.js → icons.ts} +24 -11
  11. package/src/components/menu.ts +615 -0
  12. package/src/components/{modal.js → modal.ts} +560 -237
  13. package/src/components/offcanvas.ts +457 -0
  14. package/src/components/{pagination.js → pagination.ts} +251 -76
  15. package/src/components/parabola.ts +335 -0
  16. package/src/components/{sticky.js → sticky.ts} +123 -98
  17. package/src/components/{swiper.js → swiper.ts} +510 -232
  18. package/src/components/tabs.ts +984 -0
  19. package/src/components/theme.ts +437 -0
  20. package/src/components/toast.ts +371 -0
  21. package/src/components/toc.ts +385 -0
  22. package/src/components/tooltip.ts +179 -0
  23. package/src/components/validator.ts +596 -0
  24. package/src/core/{Component.js → Component.ts} +134 -27
  25. package/src/css/accordion.css +0 -45
  26. package/src/css/animation.css +31 -1
  27. package/src/css/avatar.css +7 -33
  28. package/src/css/badge.css +11 -59
  29. package/src/css/breadcrumb.css +9 -32
  30. package/src/css/button.css +23 -224
  31. package/src/css/card.css +1 -1
  32. package/src/css/color.css +863 -1013
  33. package/src/css/color_p3.css +822 -975
  34. package/src/css/components.css +1 -0
  35. package/src/css/drop.css +7 -7
  36. package/src/css/flow.css +1 -1
  37. package/src/css/form.css +1 -1
  38. package/src/css/icon.css +7 -12
  39. package/src/css/index.css +5 -2
  40. package/src/css/loading.css +36 -20
  41. package/src/css/menu.css +1 -1
  42. package/src/css/modal.css +15 -44
  43. package/src/css/offcanvas.css +14 -40
  44. package/src/css/popup.css +4 -4
  45. package/src/css/status.css +0 -2
  46. package/src/css/swiper.css +35 -50
  47. package/src/css/tabs.css +9 -31
  48. package/src/css/tag.css +16 -77
  49. package/src/css/themes/default/accordion.css +62 -0
  50. package/src/css/themes/default/avatar.css +39 -0
  51. package/src/css/themes/default/badge.css +63 -0
  52. package/src/css/themes/default/breadcrumb.css +28 -0
  53. package/src/css/themes/default/button.css +204 -0
  54. package/src/css/themes/default/drop.css +11 -0
  55. package/src/css/themes/default/index.css +16 -0
  56. package/src/css/themes/default/loading.css +23 -0
  57. package/src/css/themes/default/modal.css +40 -0
  58. package/src/css/themes/default/offcanvas.css +13 -0
  59. package/src/css/themes/default/popup.css +7 -0
  60. package/src/css/themes/default/swiper.css +35 -0
  61. package/src/css/themes/default/tabs.css +37 -0
  62. package/src/css/themes/default/tag.css +85 -0
  63. package/src/css/themes/default/tip.css +49 -0
  64. package/src/css/themes/default/toast.css +70 -0
  65. package/src/css/themes/default/tooltip.css +14 -0
  66. package/src/css/themes/index.css +1 -0
  67. package/src/css/tip.css +20 -39
  68. package/src/css/toast.css +18 -80
  69. package/src/css/tokens.css +8 -3
  70. package/src/css/tooltip.css +10 -10
  71. package/src/index.ts +37 -0
  72. package/src/locales/{index.js → index.ts} +1 -2
  73. package/src/types.d.ts +1 -0
  74. package/src/utilities/browser.ts +131 -0
  75. package/src/utilities/cache.ts +57 -0
  76. package/src/utilities/core.ts +614 -0
  77. package/src/utilities/dom.ts +468 -0
  78. package/src/utilities/events.ts +106 -0
  79. package/src/utilities/http.ts +35 -0
  80. package/src/utilities/storage.ts +46 -0
  81. package/src/components/accordion.js +0 -373
  82. package/src/components/components.js +0 -0
  83. package/src/components/drop.js +0 -437
  84. package/src/components/flow.js +0 -1133
  85. package/src/components/form.js +0 -585
  86. package/src/components/menu.js +0 -422
  87. package/src/components/offcanvas.js +0 -332
  88. package/src/components/parabola.js +0 -295
  89. package/src/components/tabs.js +0 -746
  90. package/src/components/theme.js +0 -353
  91. package/src/components/toast.js +0 -329
  92. package/src/components/toc.js +0 -226
  93. package/src/components/tooltip.js +0 -86
  94. package/src/components/validator.js +0 -586
  95. package/src/index.js +0 -45
  96. package/src/locales/index.min.js +0 -1
  97. package/src/utilities/browser.js +0 -111
  98. package/src/utilities/cache.js +0 -159
  99. package/src/utilities/core.js +0 -502
  100. package/src/utilities/debug.js +0 -103
  101. package/src/utilities/dom.js +0 -313
  102. package/src/utilities/events.js +0 -69
  103. package/src/utilities/http.js +0 -30
  104. package/src/utilities/legacy-dom.js +0 -179
  105. package/src/utilities/storage.js +0 -46
  106. /package/src/icons/{index.js → index.ts} +0 -0
@@ -9,78 +9,274 @@ import {
9
9
  untrack,
10
10
  } from 'vanilla-signal';
11
11
 
12
- import Component from '../core/Component.js';
12
+ import Component, {
13
+ type ComponentDOM,
14
+ type ComponentRuntime,
15
+ type ComponentUpdateOptions,
16
+ } from '../core/Component.ts';
13
17
  import {
18
+ type ResolveSchema,
14
19
  hasOwn,
15
20
  isPlainObject,
16
21
  randomId,
17
22
  resolveProps,
18
23
  validateParam,
19
- } from '../utilities/core.js';
24
+ } from '../utilities/core.ts';
20
25
  import {
21
26
  all,
22
27
  q,
23
- canUseDOM,
24
- canRenderDOM,
25
28
  createLoading,
26
- isNode,
27
- requireRenderDOM,
28
- } from '../utilities/dom.js';
29
- import { Form } from './form.js';
30
- import { icon } from './icons.js';
29
+ isRenderableContent,
30
+ normalizeContentNodes,
31
+ type RenderableContent,
32
+ } from '../utilities/dom.ts';
33
+ import {
34
+ Form,
35
+ type FormDataRecord,
36
+ type FormField,
37
+ type FormProps,
38
+ } from './form.ts';
39
+ import { icon } from './icons.ts';
31
40
 
32
41
  const HIDE_DURATION = 300;
33
42
 
34
- function cloneFields(fields) {
43
+ type ModalStyle =
44
+ | string
45
+ | Record<string, string | number | null | undefined>
46
+ | null;
47
+ type ModalTextInput = Partial<ModalText> & Record<string, unknown>;
48
+ type ModalContent = RenderableContent<Modal>;
49
+ type FlowDirection = 'next' | 'back';
50
+
51
+ export interface ModalClassNames {
52
+ layout: string;
53
+ modal: string;
54
+ fullscreen: string;
55
+ header: string;
56
+ body: string;
57
+ footer: string;
58
+ title: string;
59
+ close: string;
60
+ cancel: string;
61
+ confirm: string;
62
+ button: string;
63
+ buttonIcon: string;
64
+ buttonGhost: string;
65
+ buttonPrimary: string;
66
+ buttonSmall: string;
67
+ formContainer: string;
68
+ }
69
+
70
+ export type ModalClassNameConfig = Partial<ModalClassNames>;
71
+
72
+ export interface ModalText {
73
+ title: string;
74
+ confirm: string;
75
+ cancel: string;
76
+ }
77
+
78
+ interface FlowStep {
79
+ id?: string;
80
+ title?: RenderableContent<Modal>;
81
+ content?: ModalContent;
82
+ modal?: ModalFlowView | ModalFlowViewFactory;
83
+ view?: ModalFlowView | ModalFlowViewFactory;
84
+ [key: string]: unknown;
85
+ }
86
+
87
+ interface FlowSnapshot {
88
+ data?: Record<string, unknown> | null;
89
+ currentData?: Record<string, unknown> | null;
90
+ currentStep?: FlowStep;
91
+ [key: string]: unknown;
92
+ }
93
+
94
+ interface FlowLike {
95
+ currentStep?: FlowStep;
96
+ next: (
97
+ payload: FormDataRecord | null
98
+ ) => Promise<FlowSnapshot> | FlowSnapshot;
99
+ back: (
100
+ payload: FormDataRecord | null
101
+ ) => Promise<FlowSnapshot> | FlowSnapshot;
102
+ snapshot: () => FlowSnapshot;
103
+ reset?: () => void;
104
+ destroy?: () => void;
105
+ }
106
+
107
+ interface ModalFlowView extends Partial<ModalProps> {
108
+ [key: string]: unknown;
109
+ }
110
+
111
+ type ModalFlowViewFactory = (context: {
112
+ flow: FlowLike;
113
+ snapshot: FlowSnapshot | null;
114
+ step: FlowStep | undefined;
115
+ modal: Modal;
116
+ data: Record<string, unknown> | null;
117
+ currentData: Record<string, unknown> | null;
118
+ }) => unknown;
119
+
120
+ export interface ModalProps extends Record<string, unknown> {
121
+ content?: ModalContent;
122
+ position?: string;
123
+ showCancel?: boolean;
124
+ showClose?: boolean;
125
+ fullscreen?: boolean;
126
+ flow?: FlowLike | null;
127
+ text?: ModalTextInput;
128
+ onShow?: ((modal: Modal) => void | Promise<void>) | null;
129
+ onShown?: ((modal: Modal) => void | Promise<void>) | null;
130
+ onHide?: ((modal: Modal) => void | Promise<void>) | null;
131
+ onHidden?: ((modal: Modal) => void | Promise<void>) | null;
132
+ onConfirm?: ((modal: Modal) => void | Promise<void>) | null;
133
+ onSubmit?:
134
+ | ((data: FormDataRecord, modal: Modal) => void | Promise<void>)
135
+ | null;
136
+ onCancel?: ((modal: Modal) => void | Promise<void>) | null;
137
+ fields?: readonly FormField[] | null;
138
+ header?: boolean;
139
+ footer?: boolean;
140
+ style?: ModalStyle;
141
+ id?: string | null;
142
+ escClose?: boolean;
143
+ bgClose?: boolean;
144
+ lazy?: boolean;
145
+ className?: ModalClassNameConfig;
146
+ }
147
+
148
+ interface ResolvedModalProps extends Record<string, unknown> {
149
+ content: ModalContent;
150
+ position: string;
151
+ showCancel: boolean;
152
+ showClose: boolean;
153
+ fullscreen: boolean;
154
+ flow: FlowLike | null;
155
+ text: ModalText;
156
+ onShow: NonNullable<ModalProps['onShow']> | null;
157
+ onShown: NonNullable<ModalProps['onShown']> | null;
158
+ onHide: NonNullable<ModalProps['onHide']> | null;
159
+ onHidden: NonNullable<ModalProps['onHidden']> | null;
160
+ onConfirm: NonNullable<ModalProps['onConfirm']> | null;
161
+ onSubmit: NonNullable<ModalProps['onSubmit']> | null;
162
+ onCancel: NonNullable<ModalProps['onCancel']> | null;
163
+ fields: FormField[] | null;
164
+ header: boolean;
165
+ footer: boolean;
166
+ style: ModalStyle;
167
+ id: string;
168
+ escClose: boolean;
169
+ bgClose: boolean;
170
+ lazy: boolean;
171
+ className: ModalClassNames;
172
+ }
173
+
174
+ interface ModalState extends ResolvedModalProps {
175
+ loading: boolean;
176
+ submitting: boolean;
177
+ visible: boolean;
178
+ data: FormDataRecord | null;
179
+ extraData: FormDataRecord | null;
180
+ }
181
+
182
+ interface ModalDOM extends ComponentDOM {
183
+ root: HTMLElement | null;
184
+ modal: HTMLElement | null;
185
+ header: HTMLElement | null;
186
+ body: HTMLElement | null;
187
+ footer: HTMLElement | null;
188
+ form: Form | null;
189
+ formContainer: HTMLElement | null;
190
+ }
191
+
192
+ interface ModalRuntime extends ComponentRuntime {
193
+ scrollLocked: boolean;
194
+ visibleApplied: boolean;
195
+ }
196
+
197
+ interface ModalCache {
198
+ initial: ResolvedModalProps | null;
199
+ fieldIds: Map<string, string> | null;
200
+ baseStyle: string;
201
+ previousActiveElement: HTMLElement | null;
202
+ formId: string;
203
+ }
204
+
205
+ interface ModalCleanupExtras {
206
+ visibility?: (() => void) | null;
207
+ view?: (() => void) | null;
208
+ hideTimer?: ReturnType<typeof setTimeout> | null;
209
+ }
210
+
211
+ type ModalPatch = Partial<ModalProps>;
212
+ type ModalStatePatch = Partial<ModalState>;
213
+
214
+ const DEFAULT_CLASS_NAMES: ModalClassNames = {
215
+ layout: 'j-popup-layout',
216
+ modal: 'j-modal',
217
+ fullscreen: 'is-fullscreen',
218
+ header: 'modal-header',
219
+ body: 'modal-body',
220
+ footer: 'modal-footer',
221
+ title: 'modal-title',
222
+ close: 'modal-close',
223
+ cancel: 'modal-cancel',
224
+ confirm: 'modal-confirm',
225
+ button: 'j-button',
226
+ buttonIcon: 'is-icon',
227
+ buttonGhost: 'is-ghost',
228
+ buttonPrimary: 'is-primary',
229
+ buttonSmall: 'is-sm',
230
+ formContainer: 'modal-form-container',
231
+ };
232
+
233
+ function cloneFields(
234
+ fields: readonly FormField[] | null | undefined
235
+ ): FormField[] {
35
236
  if (!Array.isArray(fields)) return [];
36
237
  return fields.map((field) => ({
37
238
  ...field,
38
239
  options: Array.isArray(field.options)
39
- ? field.options.map((option) =>
40
- option && typeof option === 'object' ? { ...option } : option
240
+ ? field.options.map(
241
+ (option: NonNullable<FormField['options']>[number]) =>
242
+ option && typeof option === 'object' ? { ...option } : option
41
243
  )
42
244
  : field.options,
43
245
  }));
44
246
  }
45
247
 
46
- function cloneProps(props) {
248
+ function cloneProps(props: ResolvedModalProps): ResolvedModalProps {
47
249
  return {
48
250
  ...props,
49
- fields: Array.isArray(props.fields)
50
- ? cloneFields(props.fields)
51
- : props.fields,
251
+ fields: Array.isArray(props.fields) ? cloneFields(props.fields) : null,
252
+ text: { ...props.text },
253
+ className: { ...props.className },
52
254
  };
53
255
  }
54
256
 
55
- function isModalContent(content) {
56
- return (
57
- content == null ||
58
- typeof content === 'string' ||
59
- typeof content === 'function' ||
60
- Array.isArray(content) ||
61
- isNode(content)
62
- );
63
- }
64
-
65
- function isUpdateProps(value) {
257
+ function isUpdateProps(value: unknown): value is ModalPatch {
66
258
  return isPlainObject(value);
67
259
  }
68
260
 
69
- function isFlowLike(value) {
261
+ function isFlowLike(value: unknown): value is FlowLike | null {
70
262
  return (
71
263
  value == null ||
72
264
  (typeof value === 'object' &&
73
- typeof value.next === 'function' &&
74
- typeof value.back === 'function' &&
75
- typeof value.snapshot === 'function')
265
+ typeof (value as Partial<FlowLike>).next === 'function' &&
266
+ typeof (value as Partial<FlowLike>).back === 'function' &&
267
+ typeof (value as Partial<FlowLike>).snapshot === 'function')
76
268
  );
77
269
  }
78
270
 
79
- function hydrateFields(fields, data) {
80
- if (!Array.isArray(fields) || !isPlainObject(data)) return fields;
271
+ function hydrateFields(
272
+ fields: readonly FormField[] | null,
273
+ data: Record<string, unknown> | null | undefined
274
+ ): FormField[] | null {
275
+ if (!Array.isArray(fields) || !isPlainObject(data))
276
+ return fields ? cloneFields(fields) : null;
81
277
 
82
278
  return fields.map((field) => {
83
- if (!field?.name || !hasOwn(data, field.name)) return field;
279
+ if (!field?.name || !hasOwn(data, field.name)) return { ...field };
84
280
  const value = data[field.name];
85
281
 
86
282
  if (field.type === 'checkbox' || field.type === 'radio') {
@@ -91,11 +287,18 @@ function hydrateFields(fields, data) {
91
287
  return { ...field, checked };
92
288
  }
93
289
 
94
- return { ...field, value };
290
+ return {
291
+ ...field,
292
+ value: Array.isArray(value)
293
+ ? value.map(String)
294
+ : (value as FormField['value']),
295
+ };
95
296
  });
96
297
  }
97
298
 
98
- function createTextState(props) {
299
+ function createTextState(
300
+ props: Partial<ModalProps> | Partial<ResolvedModalProps>
301
+ ): ModalText {
99
302
  const text = isPlainObject(props?.text) ? props.text : {};
100
303
  return {
101
304
  title: typeof text.title === 'string' ? text.title : 'Tip',
@@ -104,7 +307,7 @@ function createTextState(props) {
104
307
  };
105
308
  }
106
309
 
107
- function createModalState(props) {
310
+ function createModalState(props: ResolvedModalProps): ModalState {
108
311
  return {
109
312
  ...props,
110
313
  fields: Array.isArray(props.fields) ? cloneFields(props.fields) : null,
@@ -117,25 +320,40 @@ function createModalState(props) {
117
320
  };
118
321
  }
119
322
 
120
- function mergeExtraData(data, extraData) {
323
+ function mergeExtraData(
324
+ data: FormDataRecord,
325
+ extraData: FormDataRecord | null
326
+ ): FormDataRecord {
121
327
  if (!extraData || typeof extraData !== 'object') return data;
122
328
  return Object.assign(data, extraData);
123
329
  }
124
330
 
125
- function clonePlainObject(value) {
331
+ function clonePlainObject(value: unknown): ModalFlowView {
126
332
  return isPlainObject(value) ? { ...value } : {};
127
333
  }
128
334
 
335
+ function joinClasses(
336
+ ...classes: Array<string | false | null | undefined>
337
+ ): string {
338
+ return classes.filter(Boolean).join(' ');
339
+ }
340
+
341
+ function textValue(value: RenderableContent<Modal> | undefined): string {
342
+ return typeof value === 'string' || typeof value === 'number'
343
+ ? String(value)
344
+ : '';
345
+ }
346
+
129
347
  const MODAL_CONTENT_RULE = {
130
- validate: isModalContent,
131
- message: 'expects string, Node, array, function or null.',
348
+ validate: isRenderableContent,
349
+ message: 'expects string, number, Node, array, function or null.',
132
350
  };
133
351
 
134
352
  const MODAL_TEXT_RULE = {
135
353
  default: {},
136
- validate: (value) => isPlainObject(value),
354
+ validate: (value: unknown) => isPlainObject(value),
137
355
  message: 'expects an object with text fields.',
138
- normalize: (value) => {
356
+ normalize: (value: unknown) => {
139
357
  const text = isPlainObject(value) ? value : {};
140
358
  return {
141
359
  ...text,
@@ -173,9 +391,11 @@ const MODAL_PROPS_SCHEMA = {
173
391
  id: {
174
392
  default: null,
175
393
  types: ['string', 'null'],
176
- normalize: (value) => {
177
- if (typeof value === 'string')
178
- return value.trim() ? value.trim() : randomId();
394
+ normalize: (value: unknown) => {
395
+ if (typeof value === 'string') {
396
+ const id = value.trim();
397
+ return id || randomId();
398
+ }
179
399
  if (value == null) return randomId();
180
400
  return value;
181
401
  },
@@ -183,7 +403,15 @@ const MODAL_PROPS_SCHEMA = {
183
403
  escClose: { default: false, type: 'boolean' },
184
404
  bgClose: { default: false, type: 'boolean' },
185
405
  lazy: { default: false, type: 'boolean' },
186
- };
406
+ className: {
407
+ default: DEFAULT_CLASS_NAMES,
408
+ type: 'object',
409
+ normalize: (value: unknown) => ({
410
+ ...DEFAULT_CLASS_NAMES,
411
+ ...(value && typeof value === 'object' ? value : {}),
412
+ }),
413
+ },
414
+ } satisfies ResolveSchema<ModalProps>;
187
415
 
188
416
  const MODAL_STATE_SCHEMA = {
189
417
  ...MODAL_PROPS_SCHEMA,
@@ -197,7 +425,7 @@ const MODAL_STATE_SCHEMA = {
197
425
  const MODAL_UPDATE_BLOCKED_KEYS = new Set(['id', 'lazy']);
198
426
 
199
427
  const MODAL_EXTRA_FIELDS_RULE = {
200
- validate: (value) =>
428
+ validate: (value: unknown) =>
201
429
  !!value && typeof value === 'object' && !Array.isArray(value),
202
430
  message: 'expects an object.',
203
431
  };
@@ -207,12 +435,48 @@ const MODAL_UPDATE_RULE = {
207
435
  message: 'expects a props object.',
208
436
  };
209
437
 
438
+ function normalizeProps(input: ModalProps): ResolvedModalProps {
439
+ const props = resolveProps(input, MODAL_PROPS_SCHEMA, 'Modal');
440
+ return {
441
+ content: props.content as ModalContent,
442
+ position: props.position as string,
443
+ showCancel: props.showCancel as boolean,
444
+ showClose: props.showClose as boolean,
445
+ fullscreen: props.fullscreen as boolean,
446
+ flow: props.flow as FlowLike | null,
447
+ text: props.text as ModalText,
448
+ onShow: props.onShow as ResolvedModalProps['onShow'],
449
+ onShown: props.onShown as ResolvedModalProps['onShown'],
450
+ onHide: props.onHide as ResolvedModalProps['onHide'],
451
+ onHidden: props.onHidden as ResolvedModalProps['onHidden'],
452
+ onConfirm: props.onConfirm as ResolvedModalProps['onConfirm'],
453
+ onSubmit: props.onSubmit as ResolvedModalProps['onSubmit'],
454
+ onCancel: props.onCancel as ResolvedModalProps['onCancel'],
455
+ fields: Array.isArray(props.fields)
456
+ ? cloneFields(props.fields as FormField[])
457
+ : null,
458
+ header: props.header as boolean,
459
+ footer: props.footer as boolean,
460
+ style: props.style as ModalStyle,
461
+ id: props.id as string,
462
+ escClose: props.escClose as boolean,
463
+ bgClose: props.bgClose as boolean,
464
+ lazy: props.lazy as boolean,
465
+ className: props.className as ModalClassNames,
466
+ };
467
+ }
468
+
210
469
  let modalScrollLockCount = 0;
211
470
  let modalBodyOverflow = '';
212
471
 
213
- export class Modal extends Component {
214
- constructor(input = {}) {
215
- const props = resolveProps(input, MODAL_PROPS_SCHEMA, 'Modal');
472
+ export class Modal extends Component<ResolvedModalProps, ModalState, ModalDOM> {
473
+ declare runtime: ModalRuntime;
474
+ declare state: ModalState;
475
+ declare cleanup: Component['cleanup'] & ModalCleanupExtras;
476
+ cache: ModalCache;
477
+
478
+ constructor(input: ModalProps = {}) {
479
+ const props = normalizeProps(input);
216
480
  super(props);
217
481
 
218
482
  this.dom.modal = null;
@@ -237,38 +501,41 @@ export class Modal extends Component {
237
501
  formId: `${this.props.id}_form`,
238
502
  };
239
503
 
240
- this.state = createDeepStore(createModalState(this.props));
504
+ this.state = createDeepStore(createModalState(this.props)) as ModalState;
241
505
 
242
506
  this.init(props);
243
507
  }
244
508
 
245
- onInit() {
509
+ protected override onInit(): void {
246
510
  this.bindReactiveVisibility();
247
- if (!this.state.lazy && canRenderDOM()) this.buildRoot();
511
+ if (!this.state.lazy) this.buildRoot();
248
512
  }
249
513
 
250
- buildRoot() {
251
- if (this.root) return this.root;
514
+ buildRoot(): HTMLElement {
515
+ if (this.root instanceof HTMLElement) return this.root;
252
516
 
253
- const { id } = this.state;
517
+ const { id, className } = this.state;
254
518
  const dialogChildren = [
255
519
  jsx('div', {
256
- className: 'modal-header',
520
+ className: className.header,
521
+ 'data-modal-header': id,
257
522
  style: () => ({ display: this.state.header ? '' : 'none' }),
258
- ref: (element) => {
523
+ ref: (element: HTMLElement) => {
259
524
  this.dom.header = element;
260
525
  },
261
526
  }),
262
527
  jsx('div', {
263
- className: 'modal-body',
264
- ref: (element) => {
528
+ className: className.body,
529
+ 'data-modal-body': id,
530
+ ref: (element: HTMLElement) => {
265
531
  this.dom.body = element;
266
532
  },
267
533
  }),
268
534
  jsx('div', {
269
- className: 'modal-footer',
535
+ className: className.footer,
536
+ 'data-modal-footer': id,
270
537
  style: () => ({ display: this.state.footer ? '' : 'none' }),
271
- ref: (element) => {
538
+ ref: (element: HTMLElement) => {
272
539
  this.dom.footer = element;
273
540
  },
274
541
  }),
@@ -276,32 +543,39 @@ export class Modal extends Component {
276
543
 
277
544
  const modal = jsx('div', {
278
545
  className: () =>
279
- this.state.fullscreen ? 'j-modal is-fullscreen' : 'j-modal',
546
+ joinClasses(
547
+ this.state.className.modal,
548
+ this.state.fullscreen && this.state.className.fullscreen
549
+ ),
280
550
  id,
281
551
  role: 'document',
282
- ref: (element) => {
552
+ 'data-modal-dialog': id,
553
+ ref: (element: HTMLElement) => {
283
554
  this.dom.modal = element;
284
555
  this.applyStyle(element, this.state.style);
285
556
  },
286
557
  children: dialogChildren,
287
- });
558
+ }) as HTMLElement;
288
559
 
289
560
  const root = jsx('div', {
290
- className: () => `j-popup-layout is-${this.state.position}`,
561
+ className: () =>
562
+ joinClasses(this.state.className.layout, `is-${this.state.position}`),
291
563
  role: 'dialog',
564
+ 'data-modal': 'root',
565
+ 'data-modal-position': () => this.state.position,
292
566
  'aria-modal': 'true',
293
567
  'aria-labelledby': () => (this.state.header ? `${id}_title` : null),
294
568
  'aria-label': () =>
295
569
  this.state.header ? null : this.state.text?.title || 'Modal',
296
570
  children: modal,
297
- });
571
+ }) as HTMLElement;
298
572
 
299
573
  this.root = root;
300
574
  this.mountView();
301
575
  return root;
302
576
  }
303
577
 
304
- mountView() {
578
+ mountView(): void {
305
579
  if (this.cleanup.view || !this.dom.body) return;
306
580
 
307
581
  this.cleanup.view = createRoot((dispose) => {
@@ -310,7 +584,7 @@ export class Modal extends Component {
310
584
  onCleanup(headerDispose);
311
585
  }
312
586
 
313
- render(() => this.bodyView(), this.dom.body);
587
+ if (this.dom.body) render(() => this.bodyView(), this.dom.body);
314
588
 
315
589
  if (this.dom.footer) {
316
590
  const footerDispose = render(() => this.footerView(), this.dom.footer);
@@ -323,72 +597,88 @@ export class Modal extends Component {
323
597
  });
324
598
  }
325
599
 
326
- headerView() {
600
+ headerView(): () => (Node | null)[] | null {
327
601
  return () => {
328
602
  if (!this.state.header) return null;
329
603
 
330
604
  return [
331
605
  jsx('div', {
332
- className: 'modal-title',
606
+ className: this.state.className.title,
333
607
  id: `${this.state.id}_title`,
608
+ 'data-modal-title': this.state.id,
334
609
  children: () => this.state.text?.title,
335
- }),
610
+ }) as Node,
336
611
  this.state.showClose
337
- ? jsx('button', {
612
+ ? (jsx('button', {
338
613
  type: 'button',
339
- className: 'modal-close j-button is-icon is-ghost is-sm',
614
+ className: joinClasses(
615
+ this.state.className.button,
616
+ this.state.className.buttonIcon,
617
+ this.state.className.buttonGhost,
618
+ this.state.className.buttonSmall,
619
+ this.state.className.close
620
+ ),
340
621
  'data-action': 'close',
341
622
  'aria-label': 'close',
342
623
  children: icon('close'),
343
- })
624
+ }) as Node)
344
625
  : null,
345
626
  ];
346
627
  };
347
628
  }
348
629
 
349
- footerView() {
630
+ footerView(): () => (Node | null)[] | null {
350
631
  return () => {
351
632
  if (!this.state.footer) return null;
352
633
 
353
634
  return [
354
635
  this.state.showCancel
355
- ? jsx('button', {
636
+ ? (jsx('button', {
356
637
  type: 'button',
357
- className: 'j-button is-ghost modal-cancel',
638
+ className: joinClasses(
639
+ this.state.className.button,
640
+ this.state.className.buttonGhost,
641
+ this.state.className.cancel
642
+ ),
358
643
  'data-action': 'close',
359
644
  'aria-label': 'close',
360
645
  disabled: () => this.isBusy(),
361
646
  children: () => this.state.text?.cancel,
362
- })
647
+ }) as Node)
363
648
  : null,
364
649
  jsx('button', {
365
650
  type: () => (this.isFormMode() ? 'submit' : 'button'),
366
651
  form: () => (this.isFormMode() ? this.cache.formId : null),
367
- className: 'j-button is-primary modal-confirm',
368
- 'data-action': this.isFormMode() ? 'submit' : 'confirm',
652
+ className: joinClasses(
653
+ this.state.className.button,
654
+ this.state.className.buttonPrimary,
655
+ this.state.className.confirm
656
+ ),
657
+ 'data-action': () => (this.isFormMode() ? 'submit' : 'confirm'),
369
658
  disabled: () => this.isBusy(),
370
659
  children: () => this.state.text?.confirm,
371
- }),
660
+ }) as Node,
372
661
  ];
373
662
  };
374
663
  }
375
664
 
376
- bodyView() {
665
+ bodyView(): RenderableContent<Modal> {
377
666
  if (this.isFormMode()) return this.formView();
378
667
  this.destroyForm();
379
668
  return this.contentView(this.state.content);
380
669
  }
381
670
 
382
- formView() {
671
+ formView(): HTMLElement {
383
672
  return jsx('div', {
384
- className: 'modal-form-container',
385
- ref: (element) => {
673
+ className: this.state.className.formContainer,
674
+ 'data-modal-form-container': this.state.id,
675
+ ref: (element: HTMLElement) => {
386
676
  this.mountForm(element);
387
677
  },
388
- });
678
+ }) as HTMLElement;
389
679
  }
390
680
 
391
- mountForm(container) {
681
+ mountForm(container: HTMLElement | null): void {
392
682
  if (!container) return;
393
683
  this.dom.formContainer = container;
394
684
 
@@ -401,7 +691,7 @@ export class Modal extends Component {
401
691
  this.dom.form = new Form(props, container).build();
402
692
  }
403
693
 
404
- createFormProps() {
694
+ createFormProps(): FormProps {
405
695
  return {
406
696
  id: this.cache.formId,
407
697
  fields: this.state.fields || [],
@@ -410,31 +700,23 @@ export class Modal extends Component {
410
700
  };
411
701
  }
412
702
 
413
- destroyForm() {
703
+ destroyForm(): void {
414
704
  this.dom.form?.destroy();
415
705
  this.dom.form = null;
416
706
  this.dom.formContainer = null;
417
707
  }
418
708
 
419
- contentView(content) {
420
- if (typeof content === 'function') return content(this);
421
- if (Array.isArray(content) || isNode(content)) return content;
422
- if (content == null) return '';
423
- if (typeof content !== 'string') return '';
424
-
425
- // return Array.from(html(content).childNodes);
426
- const template = document.createElement('template');
427
- template.innerHTML = content;
428
- return Array.from(template.content.childNodes);
709
+ contentView(content: ModalContent): Node[] {
710
+ return normalizeContentNodes(content, this);
429
711
  }
430
712
 
431
- bindReactiveLoading() {
432
- let loading = null;
713
+ bindReactiveLoading(): void {
714
+ let loading: HTMLElement | null = null;
433
715
 
434
716
  createEffect(() => {
435
717
  if (this.state.loading && !loading) {
436
718
  loading = createLoading();
437
- this.dom.modal.appendChild(loading);
719
+ this.dom.modal?.appendChild(loading);
438
720
  } else if (!this.state.loading && loading) {
439
721
  loading.remove();
440
722
  loading = null;
@@ -447,22 +729,22 @@ export class Modal extends Component {
447
729
  });
448
730
  }
449
731
 
450
- bindReactiveStyle() {
732
+ bindReactiveStyle(): void {
451
733
  createEffect(() => {
452
734
  const style = this.state.style;
453
735
  if (this.dom.modal) this.applyStyle(this.dom.modal, style);
454
736
  });
455
737
  }
456
738
 
457
- isFormMode() {
739
+ isFormMode(): boolean {
458
740
  return Array.isArray(this.state.fields);
459
741
  }
460
742
 
461
- isBusy() {
743
+ isBusy(): boolean {
462
744
  return !!(this.state.loading || this.state.submitting);
463
745
  }
464
746
 
465
- validatePropsPatch(patch, namespace = 'Modal.update') {
747
+ validatePropsPatch(patch: ModalPatch, namespace = 'Modal.update'): void {
466
748
  validateParam('props', patch, MODAL_UPDATE_RULE, namespace);
467
749
 
468
750
  for (const key of Object.keys(patch)) {
@@ -476,56 +758,75 @@ export class Modal extends Component {
476
758
  `Modal.update: "${key}" cannot be updated after initialization.`
477
759
  );
478
760
  }
479
- validateParam(key, patch[key], MODAL_PROPS_SCHEMA[key], namespace);
761
+ validateParam(
762
+ key,
763
+ patch[key],
764
+ MODAL_PROPS_SCHEMA[key as keyof typeof MODAL_PROPS_SCHEMA],
765
+ namespace
766
+ );
480
767
  }
481
768
  }
482
769
 
483
- applyProps(patch, { validate = true, force = false } = {}) {
770
+ applyProps(
771
+ patch: ModalPatch,
772
+ {
773
+ validate = true,
774
+ force = false,
775
+ }: { validate?: boolean; force?: boolean } = {}
776
+ ): this {
484
777
  if (validate) this.validatePropsPatch(patch);
485
778
  if (!patch || Object.keys(patch).length === 0) return this;
486
779
 
487
780
  const hasFields = hasOwn(patch, 'fields');
488
781
  const hasContent = hasOwn(patch, 'content');
489
782
 
490
- if (hasContent) {
491
- if (!hasFields && this.isFormMode() && !force) {
492
- throw new Error(
493
- 'Modal.update: Cannot update content when fields are defined.'
494
- );
495
- }
783
+ if (hasContent && !hasFields && this.isFormMode() && !force) {
784
+ throw new Error(
785
+ 'Modal.update: Cannot update content when fields are defined.'
786
+ );
496
787
  }
497
788
 
498
- const nextProps = Object.assign({}, this.props, patch);
499
- if (hasOwn(patch, 'text')) {
500
- nextProps.text = Object.assign({}, this.props.text || {}, patch.text);
501
- }
789
+ const nextInput: ModalProps = {
790
+ ...(this.props as ModalProps),
791
+ ...patch,
792
+ text: (hasOwn(patch, 'text')
793
+ ? { ...this.props.text, ...patch.text }
794
+ : this.props.text) as ModalTextInput,
795
+ className: hasOwn(patch, 'className')
796
+ ? { ...this.props.className, ...patch.className }
797
+ : this.props.className,
798
+ };
799
+ const nextProps = normalizeProps(nextInput);
502
800
 
503
801
  this.props = nextProps;
504
- super.update(patch, { force });
802
+ super.update(patch as Partial<ResolvedModalProps>, { force });
505
803
  this.props = nextProps;
506
804
 
507
- const statePatch = {};
805
+ const statePatch: ModalStatePatch = {};
508
806
  if (hasFields) {
509
- this.cache.fieldIds.clear();
807
+ this.cache.fieldIds?.clear();
510
808
  statePatch.fields = Array.isArray(patch.fields)
511
809
  ? cloneFields(patch.fields)
512
810
  : null;
513
811
  }
514
812
 
515
813
  for (const [key, value] of Object.entries(patch)) {
516
- if (key === 'fields' || key === 'text') continue;
517
- statePatch[key] = value;
814
+ if (key === 'fields' || key === 'text' || key === 'className') continue;
815
+ statePatch[key as keyof ModalStatePatch] =
816
+ value as ModalStatePatch[keyof ModalStatePatch];
518
817
  }
519
818
 
520
- const shouldRefreshText = hasOwn(patch, 'text');
521
-
522
- if (shouldRefreshText) {
523
- statePatch.text = createTextState(this.props);
819
+ if (hasOwn(patch, 'text')) {
820
+ statePatch.text = createTextState(nextProps);
821
+ }
822
+ if (hasOwn(patch, 'className')) {
823
+ statePatch.className = nextProps.className;
524
824
  }
525
825
 
526
826
  flushSync(() => {
527
827
  for (const [key, value] of Object.entries(statePatch)) {
528
- this.state[key] = value;
828
+ this.state[key as keyof ModalState] =
829
+ value as ModalState[keyof ModalState];
529
830
  }
530
831
  });
531
832
 
@@ -547,7 +848,7 @@ export class Modal extends Component {
547
848
  return this;
548
849
  }
549
850
 
550
- applyStyle(element, style) {
851
+ applyStyle(element: HTMLElement, style: ModalStyle): void {
551
852
  element.removeAttribute('style');
552
853
  if (!style) {
553
854
  this.cache.baseStyle = '';
@@ -558,27 +859,17 @@ export class Modal extends Component {
558
859
  this.cache.baseStyle = element.getAttribute('style') || '';
559
860
  return;
560
861
  }
561
- if (typeof style === 'object') {
562
- Object.entries(style).forEach(([key, value]) => {
563
- if (value == null) return;
564
- const name = key.startsWith('--')
565
- ? key
566
- : key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
567
- element.style.setProperty(name, String(value));
568
- });
569
- }
862
+ Object.entries(style).forEach(([key, value]) => {
863
+ if (value == null) return;
864
+ const name = key.startsWith('--')
865
+ ? key
866
+ : key.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
867
+ element.style.setProperty(name, String(value));
868
+ });
570
869
  this.cache.baseStyle = element.getAttribute('style') || '';
571
870
  }
572
871
 
573
- // normalizeIcon(element) {
574
- // if (!(element instanceof Element)) return;
575
- // const iconElement = q('.modal-close', element);
576
- // if (iconElement) {
577
- // iconElement.style.transform = `translateX(${iconElement.clientHeight / 3}px)`;
578
- // }
579
- // }
580
-
581
- bindReactiveVisibility() {
872
+ bindReactiveVisibility(): void {
582
873
  this.cleanup.visibility = createRoot((dispose) => {
583
874
  createEffect(() => {
584
875
  const visible = !!this.state.visible;
@@ -588,7 +879,7 @@ export class Modal extends Component {
588
879
  });
589
880
  }
590
881
 
591
- applyVisibility(visible) {
882
+ applyVisibility(visible: boolean): void {
592
883
  if (visible === this.runtime.visibleApplied) return;
593
884
  if (visible) {
594
885
  this.showFromState();
@@ -597,38 +888,39 @@ export class Modal extends Component {
597
888
  this.hideFromState();
598
889
  }
599
890
 
600
- showFromState() {
891
+ showFromState(): void {
601
892
  if (this.runtime.destroyed) {
602
893
  throw new Error('Modal: The current instance has been destroyed.');
603
894
  }
604
895
 
605
- requireRenderDOM('Modal.show');
606
-
607
896
  if (!this.root) this.buildRoot();
608
897
 
609
898
  this.cancelHideTimer();
610
899
  this.resetAnimationStyles();
611
900
 
612
901
  const { onShow, onShown } = this.state;
613
- if (onShow) onShow();
902
+ void onShow?.(this);
614
903
 
615
- this.cache.previousActiveElement = document.activeElement;
616
- if (!this.root.parentNode) document.body.appendChild(this.root);
904
+ this.cache.previousActiveElement =
905
+ document.activeElement instanceof HTMLElement
906
+ ? document.activeElement
907
+ : null;
908
+ if (this.root && !this.root.parentNode)
909
+ document.body.appendChild(this.root);
617
910
  this.lockScroll();
618
911
  this.runtime.visibleApplied = true;
619
912
 
620
- // this.normalizeIcon(this.dom.modal);
621
913
  this.bindEvents(this.root);
622
914
  this.focusFirst();
623
915
 
624
- if (onShown) onShown();
916
+ void onShown?.(this);
625
917
  }
626
918
 
627
- hideFromState() {
919
+ hideFromState(): void {
628
920
  if (!this.runtime.visibleApplied || !this.root) return;
629
921
 
630
922
  const { onHide, onHidden } = this.state;
631
- if (onHide) onHide();
923
+ void onHide?.(this);
632
924
 
633
925
  this.runtime.visibleApplied = false;
634
926
  this.clearEvents();
@@ -652,14 +944,14 @@ export class Modal extends Component {
652
944
  );
653
945
  }
654
946
 
655
- bindEvents(root) {
947
+ bindEvents(root: Element | null): void {
656
948
  this.clearEvents();
657
949
  this.bindOverlayCloseEvent(root);
658
950
  this.bindDocumentKeyEvent();
659
951
  this.bindInsideEvent();
660
952
  }
661
953
 
662
- bindOverlayCloseEvent(root) {
954
+ bindOverlayCloseEvent(root: Element | null): void {
663
955
  if (!root) return;
664
956
  this.cleanup.events.on('bg', root, 'click', (event) => {
665
957
  if (this.state.bgClose && event.target === root) {
@@ -668,9 +960,9 @@ export class Modal extends Component {
668
960
  });
669
961
  }
670
962
 
671
- bindDocumentKeyEvent() {
963
+ bindDocumentKeyEvent(): void {
672
964
  this.cleanup.events.on('keydown', document, 'keydown', (event) => {
673
- if (!this.state.visible) return;
965
+ if (!this.state.visible || !(event instanceof KeyboardEvent)) return;
674
966
 
675
967
  if (event.key === 'Escape' && this.state.escClose) {
676
968
  event.preventDefault();
@@ -682,20 +974,16 @@ export class Modal extends Component {
682
974
  });
683
975
  }
684
976
 
685
- bindInsideEvent() {
977
+ bindInsideEvent(): void {
686
978
  if (!this.dom.modal) return;
687
979
 
688
980
  this.cleanup.events.on('inside', this.dom.modal, 'click', (event) => {
689
981
  const target = event.target instanceof Element ? event.target : null;
690
- const actionEl = target?.closest('[data-action]');
691
- if (!actionEl || !this.dom.modal.contains(actionEl)) return;
982
+ const actionEl = target?.closest<HTMLElement>('[data-action]');
983
+ if (!actionEl || !this.dom.modal?.contains(actionEl)) return;
692
984
 
693
985
  const action = actionEl.dataset.action;
694
- if (
695
- action === 'cancel' ||
696
- action === 'close' ||
697
- actionEl.classList.contains('modal-close')
698
- ) {
986
+ if (action === 'cancel' || action === 'close') {
699
987
  void this.handleCancel();
700
988
  return;
701
989
  }
@@ -717,7 +1005,6 @@ export class Modal extends Component {
717
1005
 
718
1006
  if (action === 'confirm') {
719
1007
  if (this.isFormMode()) {
720
- if (actionEl.classList.contains('modal-confirm')) return;
721
1008
  this.requestSubmit();
722
1009
  return;
723
1010
  }
@@ -726,11 +1013,11 @@ export class Modal extends Component {
726
1013
  });
727
1014
  }
728
1015
 
729
- clearEvents() {
1016
+ clearEvents(): void {
730
1017
  this.cleanup.events.clear();
731
1018
  }
732
1019
 
733
- requestSubmit() {
1020
+ requestSubmit(): void {
734
1021
  if (!this.dom.form) {
735
1022
  void this.handleConfirm();
736
1023
  return;
@@ -739,7 +1026,7 @@ export class Modal extends Component {
739
1026
  this.dom.form.requestSubmit();
740
1027
  }
741
1028
 
742
- async handleFormSubmit(formData) {
1029
+ async handleFormSubmit(formData: FormDataRecord): Promise<void> {
743
1030
  if (this.isBusy()) return;
744
1031
 
745
1032
  const data = mergeExtraData(formData, this.state.extraData);
@@ -751,23 +1038,24 @@ export class Modal extends Component {
751
1038
  await this.handleSubmit(data);
752
1039
  }
753
1040
 
754
- async handleNext() {
1041
+ async handleNext(): Promise<void> {
755
1042
  if (this.isBusy()) return;
756
1043
  if (!this.hasFlow()) return;
757
1044
  await this.moveFlow('next');
758
1045
  }
759
1046
 
760
- async handleBack() {
1047
+ async handleBack(): Promise<void> {
761
1048
  if (this.isBusy()) return;
762
1049
  if (!this.hasFlow()) return;
763
1050
  await this.moveFlow('back');
764
1051
  }
765
1052
 
766
- hasFlow() {
1053
+ hasFlow(): this is this & { state: ModalState & { flow: FlowLike } } {
767
1054
  return isFlowLike(this.state.flow) && !!this.state.flow;
768
1055
  }
769
1056
 
770
- async moveFlow(direction) {
1057
+ async moveFlow(direction: FlowDirection): Promise<void> {
1058
+ if (!this.hasFlow()) return;
771
1059
  const payload = this.createFlowPayload();
772
1060
  if (payload === false) return;
773
1061
 
@@ -776,7 +1064,9 @@ export class Modal extends Component {
776
1064
  });
777
1065
 
778
1066
  try {
779
- const snapshot = await this.state.flow[direction](payload);
1067
+ const snapshot = await Promise.resolve(
1068
+ this.state.flow[direction](payload)
1069
+ );
780
1070
  this.syncFlowView(this.state.flow, snapshot);
781
1071
  } catch (error) {
782
1072
  console.error(`Modal.flow.${direction} error:`, error);
@@ -790,7 +1080,7 @@ export class Modal extends Component {
790
1080
  }
791
1081
  }
792
1082
 
793
- createFlowPayload() {
1083
+ createFlowPayload(): FormDataRecord | null | false {
794
1084
  if (!this.isFormMode()) return null;
795
1085
 
796
1086
  const form = this.dom.form;
@@ -805,7 +1095,11 @@ export class Modal extends Component {
805
1095
  return this.state.data;
806
1096
  }
807
1097
 
808
- resolveFlowModalView(flow, snapshot, step) {
1098
+ resolveFlowModalView(
1099
+ flow: FlowLike,
1100
+ snapshot: FlowSnapshot | null,
1101
+ step?: FlowStep
1102
+ ): ModalFlowView {
809
1103
  const source = step?.modal ?? step?.view;
810
1104
  if (typeof source === 'function') {
811
1105
  const result = source({
@@ -821,7 +1115,10 @@ export class Modal extends Component {
821
1115
  return clonePlainObject(source);
822
1116
  }
823
1117
 
824
- syncFlowView(flow, snapshot = null) {
1118
+ syncFlowView(
1119
+ flow: FlowLike | null,
1120
+ snapshot: FlowSnapshot | null = null
1121
+ ): void {
825
1122
  if (!flow || !this.state) return;
826
1123
  const state = flow.snapshot() || snapshot;
827
1124
  const step =
@@ -832,12 +1129,12 @@ export class Modal extends Component {
832
1129
  (!hasOwn(modalView, 'text') || !hasOwn(nextText, 'title')) &&
833
1130
  step?.title != null;
834
1131
 
835
- const patch = {
1132
+ const patch: ModalPatch = {
836
1133
  ...(shouldInjectStepTitle
837
1134
  ? {
838
1135
  text: {
839
1136
  ...nextText,
840
- title: step.title,
1137
+ title: textValue(step.title),
841
1138
  },
842
1139
  }
843
1140
  : {}),
@@ -857,7 +1154,7 @@ export class Modal extends Component {
857
1154
  this.update(patch);
858
1155
  }
859
1156
 
860
- async handleConfirm() {
1157
+ async handleConfirm(): Promise<void> {
861
1158
  if (this.isBusy()) return;
862
1159
 
863
1160
  flushSync(() => {
@@ -865,7 +1162,7 @@ export class Modal extends Component {
865
1162
  });
866
1163
 
867
1164
  try {
868
- if (this.state.onConfirm) await Promise.resolve(this.state.onConfirm());
1165
+ await Promise.resolve(this.state.onConfirm?.(this));
869
1166
  this.hide();
870
1167
  } catch (error) {
871
1168
  console.error('Modal.onConfirm error:', error);
@@ -878,7 +1175,7 @@ export class Modal extends Component {
878
1175
  }
879
1176
  }
880
1177
 
881
- async handleCancel() {
1178
+ async handleCancel(): Promise<void> {
882
1179
  if (this.isBusy()) return;
883
1180
 
884
1181
  flushSync(() => {
@@ -886,7 +1183,7 @@ export class Modal extends Component {
886
1183
  });
887
1184
 
888
1185
  try {
889
- if (this.state.onCancel) await Promise.resolve(this.state.onCancel(this));
1186
+ await Promise.resolve(this.state.onCancel?.(this));
890
1187
  this.hide();
891
1188
  } catch (error) {
892
1189
  console.error('Modal.onCancel error:', error);
@@ -899,7 +1196,7 @@ export class Modal extends Component {
899
1196
  }
900
1197
  }
901
1198
 
902
- async handleSubmit(data) {
1199
+ async handleSubmit(data: FormDataRecord): Promise<void> {
903
1200
  if (!this.state.onSubmit) return;
904
1201
 
905
1202
  flushSync(() => {
@@ -907,7 +1204,7 @@ export class Modal extends Component {
907
1204
  });
908
1205
 
909
1206
  try {
910
- await Promise.resolve(this.state.onSubmit(data));
1207
+ await Promise.resolve(this.state.onSubmit(data, this));
911
1208
  this.state.extraData = null;
912
1209
  } catch (error) {
913
1210
  console.error('Modal.onSubmit error:', error);
@@ -920,10 +1217,10 @@ export class Modal extends Component {
920
1217
  }
921
1218
  }
922
1219
 
923
- trapFocus(event) {
1220
+ trapFocus(event: KeyboardEvent): void {
924
1221
  if (!this.dom.modal) return;
925
1222
  const focusable = Array.from(
926
- all(
1223
+ all<HTMLElement>(
927
1224
  'a[href], button:not([disabled]):not([data-action=close]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',
928
1225
  this.dom.modal
929
1226
  )
@@ -943,17 +1240,17 @@ export class Modal extends Component {
943
1240
 
944
1241
  if (event.shiftKey && document.activeElement === first) {
945
1242
  event.preventDefault();
946
- last.focus();
1243
+ last?.focus();
947
1244
  } else if (!event.shiftKey && document.activeElement === last) {
948
1245
  event.preventDefault();
949
- first.focus();
1246
+ first?.focus();
950
1247
  }
951
1248
  }
952
1249
 
953
- focusFirst() {
1250
+ focusFirst(): void {
954
1251
  if (!this.dom.modal) return;
955
1252
  const focusRoot = this.dom.form?.root || this.dom.modal;
956
- const firstFocusable = q(
1253
+ const firstFocusable = q<HTMLElement>(
957
1254
  'input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]):not([data-action=close]), [tabindex]:not([tabindex="-1"])',
958
1255
  focusRoot
959
1256
  );
@@ -964,8 +1261,8 @@ export class Modal extends Component {
964
1261
  }
965
1262
  }
966
1263
 
967
- lockScroll() {
968
- if (this.runtime.scrollLocked || !canUseDOM()) return;
1264
+ lockScroll(): void {
1265
+ if (this.runtime.scrollLocked) return;
969
1266
  if (modalScrollLockCount === 0) {
970
1267
  modalBodyOverflow = document.body.style.overflow;
971
1268
  document.body.style.overflow = 'hidden';
@@ -974,8 +1271,8 @@ export class Modal extends Component {
974
1271
  this.runtime.scrollLocked = true;
975
1272
  }
976
1273
 
977
- unlockScroll() {
978
- if (!this.runtime.scrollLocked || !canUseDOM()) return;
1274
+ unlockScroll(): void {
1275
+ if (!this.runtime.scrollLocked) return;
979
1276
  modalScrollLockCount = Math.max(0, modalScrollLockCount - 1);
980
1277
  if (modalScrollLockCount === 0) {
981
1278
  document.body.style.overflow = modalBodyOverflow;
@@ -984,29 +1281,29 @@ export class Modal extends Component {
984
1281
  this.runtime.scrollLocked = false;
985
1282
  }
986
1283
 
987
- cancelHideTimer() {
1284
+ cancelHideTimer(): void {
988
1285
  if (!this.cleanup.hideTimer) return;
989
1286
  clearTimeout(this.cleanup.hideTimer);
990
1287
  this.cleanup.hideTimer = null;
991
1288
  }
992
1289
 
993
- resetAnimationStyles() {
1290
+ resetAnimationStyles(): void {
994
1291
  if (!this.dom.modal) return;
995
1292
  if (this.cache.baseStyle)
996
1293
  this.dom.modal.setAttribute('style', this.cache.baseStyle);
997
1294
  else this.dom.modal.removeAttribute('style');
998
1295
  }
999
1296
 
1000
- finishHide(onHidden) {
1297
+ finishHide(onHidden: ResolvedModalProps['onHidden']): void {
1001
1298
  this.cleanup.hideTimer = null;
1002
- if (this.root?.parentNode) this.root.parentNode.removeChild(this.root);
1299
+ this.root?.remove();
1003
1300
  this.resetAnimationStyles();
1004
1301
  this.unlockScroll();
1005
1302
  this.restoreFocus();
1006
- if (onHidden) onHidden();
1303
+ void onHidden?.(this);
1007
1304
  }
1008
1305
 
1009
- restoreFocus() {
1306
+ restoreFocus(): void {
1010
1307
  const target = this.cache.previousActiveElement;
1011
1308
  this.cache.previousActiveElement = null;
1012
1309
  if (
@@ -1018,7 +1315,7 @@ export class Modal extends Component {
1018
1315
  }
1019
1316
  }
1020
1317
 
1021
- assertActive(method) {
1318
+ assertActive(method: string): void {
1022
1319
  if (this.runtime.destroyed) {
1023
1320
  throw new Error(
1024
1321
  `Modal.${method}: The current instance has been destroyed.`
@@ -1026,7 +1323,7 @@ export class Modal extends Component {
1026
1323
  }
1027
1324
  }
1028
1325
 
1029
- validateStatePatch(patch) {
1326
+ validateStatePatch(patch: ModalStatePatch): void {
1030
1327
  if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
1031
1328
  throw new Error('Modal.setState: expects a plain object patch.');
1032
1329
  }
@@ -1042,25 +1339,34 @@ export class Modal extends Component {
1042
1339
  `Modal.setState: "${key}" cannot be updated after initialization.`
1043
1340
  );
1044
1341
  }
1045
- validateParam(key, patch[key], MODAL_STATE_SCHEMA[key], 'Modal.setState');
1342
+ validateParam(
1343
+ key,
1344
+ patch[key as keyof ModalStatePatch],
1345
+ MODAL_STATE_SCHEMA[key as keyof typeof MODAL_STATE_SCHEMA],
1346
+ 'Modal.setState'
1347
+ );
1046
1348
  }
1047
1349
  }
1048
1350
 
1049
- setState(patch = {}) {
1351
+ override setState(patch: ModalStatePatch = {}): this {
1050
1352
  this.assertActive('setState');
1051
1353
  this.validateStatePatch(patch);
1052
1354
 
1053
1355
  flushSync(() => {
1054
1356
  for (const [key, value] of Object.entries(patch)) {
1055
- if (key === 'fields') this.cache.fieldIds.clear();
1056
- this.state[key] =
1057
- key === 'fields' && Array.isArray(value) ? cloneFields(value) : value;
1357
+ if (key === 'fields') this.cache.fieldIds?.clear();
1358
+ this.state[key as keyof ModalState] =
1359
+ key === 'fields' && Array.isArray(value)
1360
+ ? (cloneFields(
1361
+ value as FormField[]
1362
+ ) as ModalState[keyof ModalState])
1363
+ : (value as ModalState[keyof ModalState]);
1058
1364
  }
1059
1365
  });
1060
1366
  return this;
1061
1367
  }
1062
1368
 
1063
- show() {
1369
+ show(): this {
1064
1370
  this.assertActive('show');
1065
1371
  flushSync(() => {
1066
1372
  this.state.visible = true;
@@ -1068,7 +1374,7 @@ export class Modal extends Component {
1068
1374
  return this;
1069
1375
  }
1070
1376
 
1071
- hide() {
1377
+ hide(): this {
1072
1378
  this.assertActive('hide');
1073
1379
  flushSync(() => {
1074
1380
  this.state.visible = false;
@@ -1076,13 +1382,13 @@ export class Modal extends Component {
1076
1382
  return this;
1077
1383
  }
1078
1384
 
1079
- setFields(data, force = false) {
1385
+ setFields(data: readonly FormField[] | null, force = false): this {
1080
1386
  validateParam('data', data, MODAL_FIELDS_RULE, 'Modal.setFields');
1081
1387
  this.applyProps({ fields: data }, { validate: false, force });
1082
1388
  return this;
1083
1389
  }
1084
1390
 
1085
- addFields(data) {
1391
+ addFields(data: FormDataRecord): this {
1086
1392
  validateParam('data', data, MODAL_EXTRA_FIELDS_RULE, 'Modal.addFields');
1087
1393
  flushSync(() => {
1088
1394
  this.state.extraData = data;
@@ -1090,7 +1396,7 @@ export class Modal extends Component {
1090
1396
  return this;
1091
1397
  }
1092
1398
 
1093
- setContent(content, force = false) {
1399
+ setContent(content: ModalContent, force = false): this {
1094
1400
  validateParam('content', content, MODAL_CONTENT_RULE, 'Modal.setContent');
1095
1401
 
1096
1402
  if (this.isFormMode() && !force) {
@@ -1103,18 +1409,35 @@ export class Modal extends Component {
1103
1409
  return this;
1104
1410
  }
1105
1411
 
1106
- update(patch = {}, force = false) {
1107
- return this.applyProps(patch, { validate: true, force });
1412
+ override update(
1413
+ patch?: Partial<ResolvedModalProps> | null,
1414
+ options?: ComponentUpdateOptions
1415
+ ): this;
1416
+ update(patch?: ModalPatch | null, force?: boolean): this;
1417
+ update(
1418
+ patch: ModalPatch | Partial<ResolvedModalProps> | null = {},
1419
+ forceOrOptions: boolean | ComponentUpdateOptions = false
1420
+ ): this {
1421
+ const force =
1422
+ typeof forceOrOptions === 'boolean'
1423
+ ? forceOrOptions
1424
+ : !!forceOrOptions.force;
1425
+ return this.applyProps((patch || {}) as ModalPatch, {
1426
+ validate: true,
1427
+ force,
1428
+ });
1108
1429
  }
1109
1430
 
1110
- reset() {
1111
- this.cache.fieldIds.clear();
1431
+ reset(): this {
1432
+ this.cache.fieldIds?.clear();
1433
+ if (!this.cache.initial) return this;
1112
1434
 
1113
1435
  const initialProps = cloneProps(this.cache.initial);
1114
- delete initialProps.id;
1115
- delete initialProps.lazy;
1116
- this.props = cloneProps(initialProps);
1117
- this.applyProps(initialProps, { validate: false });
1436
+ const patch: ModalPatch = { ...(initialProps as unknown as ModalProps) };
1437
+ delete patch.id;
1438
+ delete patch.lazy;
1439
+ this.props = normalizeProps(patch);
1440
+ this.applyProps(patch, { validate: false });
1118
1441
  flushSync(() => {
1119
1442
  this.state.data = null;
1120
1443
  this.state.extraData = null;
@@ -1122,11 +1445,11 @@ export class Modal extends Component {
1122
1445
  return this;
1123
1446
  }
1124
1447
 
1125
- resetContent() {
1448
+ resetContent(): this {
1126
1449
  return this.setContent(this.cache.initial?.content ?? '');
1127
1450
  }
1128
1451
 
1129
- resetFields() {
1452
+ resetFields(): this {
1130
1453
  return this.setFields(
1131
1454
  Array.isArray(this.cache.initial?.fields)
1132
1455
  ? cloneFields(this.cache.initial.fields)
@@ -1134,12 +1457,12 @@ export class Modal extends Component {
1134
1457
  );
1135
1458
  }
1136
1459
 
1137
- onDestroy() {
1460
+ protected override onDestroy(): void {
1138
1461
  const wasVisible = !!this.runtime.visibleApplied;
1139
1462
  const onHide = this.state?.onHide;
1140
1463
  const onHidden = this.state?.onHidden;
1141
1464
 
1142
- if (wasVisible && onHide) onHide();
1465
+ if (wasVisible) void onHide?.(this);
1143
1466
 
1144
1467
  this.cancelHideTimer();
1145
1468
  this.clearEvents();
@@ -1150,8 +1473,8 @@ export class Modal extends Component {
1150
1473
  this.cleanup.view?.();
1151
1474
  this.cleanup.view = null;
1152
1475
 
1153
- if (this.root?.parentNode) this.root.parentNode.removeChild(this.root);
1154
- if (wasVisible && onHidden) onHidden();
1476
+ this.root?.remove();
1477
+ if (wasVisible) void onHidden?.(this);
1155
1478
 
1156
1479
  this.cache = {
1157
1480
  initial: null,
@@ -1162,13 +1485,13 @@ export class Modal extends Component {
1162
1485
  };
1163
1486
  }
1164
1487
 
1165
- destroy() {
1166
- if (this.runtime.destroyed) return;
1488
+ override destroy(): this {
1489
+ if (this.runtime.destroyed) return this;
1167
1490
  super.destroy();
1168
1491
  return this;
1169
1492
  }
1170
1493
  }
1171
1494
 
1172
- export function createModal(input) {
1495
+ export function createModal(input: ModalProps = {}): Modal {
1173
1496
  return new Modal(input);
1174
1497
  }