@elementor/editor-ui 3.33.0-117 → 3.33.0-119

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/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as React$1 from 'react';
2
- import { ReactNode, PropsWithChildren } from 'react';
3
- import { MenuItemProps, AlertProps, InfotipProps, MenuList } from '@elementor/ui';
2
+ import { ReactNode, PropsWithChildren, ReactElement } from 'react';
3
+ import { MenuItemProps, AlertProps, InfotipProps, MenuList, DialogProps, DialogContentTextProps } from '@elementor/ui';
4
4
  import * as _emotion_styled from '@emotion/styled';
5
5
 
6
6
  type EllipsisWithTooltipProps<T extends React$1.ElementType> = {
@@ -58,6 +58,14 @@ interface WarningInfotipProps extends PropsWithChildren {
58
58
  }
59
59
  declare const WarningInfotip: React$1.ForwardRefExoticComponent<WarningInfotipProps & React$1.RefAttributes<unknown>>;
60
60
 
61
+ declare const GlobalDialog: () => React$1.JSX.Element | null;
62
+
63
+ type DialogContent = {
64
+ component: ReactElement;
65
+ };
66
+ declare const openDialog: ({ component }: DialogContent) => void;
67
+ declare const closeDialog: () => void;
68
+
61
69
  type PopoverBodyProps = PropsWithChildren<{
62
70
  height?: number | 'auto';
63
71
  width?: number;
@@ -108,6 +116,32 @@ type Props = {
108
116
  };
109
117
  declare const PopoverSearch: ({ value, onSearch, placeholder }: Props) => React$1.JSX.Element;
110
118
 
119
+ declare const SaveChangesDialog: {
120
+ ({ children, onClose }: Pick<DialogProps, "children" | "onClose">): React$1.JSX.Element;
121
+ Title: ({ children, onClose }: React$1.PropsWithChildren & {
122
+ onClose?: () => void;
123
+ }) => React$1.JSX.Element;
124
+ Content: ({ children }: React$1.PropsWithChildren) => React$1.JSX.Element;
125
+ ContentText: (props: DialogContentTextProps) => React$1.JSX.Element;
126
+ Actions: ({ actions }: ConfirmationDialogActionsProps) => React$1.JSX.Element;
127
+ };
128
+ type Action = {
129
+ label: string;
130
+ action: () => void | Promise<void>;
131
+ };
132
+ type ConfirmationDialogActionsProps = {
133
+ actions: {
134
+ cancel?: Action;
135
+ confirm: Action;
136
+ discard?: Action;
137
+ };
138
+ };
139
+ declare const useDialog: () => {
140
+ isOpen: boolean;
141
+ open: () => void;
142
+ close: () => void;
143
+ };
144
+
111
145
  type UseEditableParams = {
112
146
  value: string;
113
147
  onSubmit: (value: string) => unknown;
@@ -134,4 +168,4 @@ declare const useEditable: ({ value, onSubmit, validation, onClick, onError }: U
134
168
  };
135
169
  };
136
170
 
137
- export { EditableField, EllipsisWithTooltip, ITEM_HEIGHT, InfoAlert, InfoTipCard, IntroductionModal, MenuItemInfotip, MenuListItem, PopoverBody, PopoverHeader, PopoverMenuList, type PopoverMenuListProps, PopoverSearch, StyledMenuList, ThemeProvider, type VirtualizedItem, WarningInfotip, useEditable };
171
+ export { EditableField, EllipsisWithTooltip, GlobalDialog, ITEM_HEIGHT, InfoAlert, InfoTipCard, IntroductionModal, MenuItemInfotip, MenuListItem, PopoverBody, PopoverHeader, PopoverMenuList, type PopoverMenuListProps, PopoverSearch, SaveChangesDialog, StyledMenuList, ThemeProvider, type VirtualizedItem, WarningInfotip, closeDialog, openDialog, useDialog, useEditable };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as React$1 from 'react';
2
- import { ReactNode, PropsWithChildren } from 'react';
3
- import { MenuItemProps, AlertProps, InfotipProps, MenuList } from '@elementor/ui';
2
+ import { ReactNode, PropsWithChildren, ReactElement } from 'react';
3
+ import { MenuItemProps, AlertProps, InfotipProps, MenuList, DialogProps, DialogContentTextProps } from '@elementor/ui';
4
4
  import * as _emotion_styled from '@emotion/styled';
5
5
 
6
6
  type EllipsisWithTooltipProps<T extends React$1.ElementType> = {
@@ -58,6 +58,14 @@ interface WarningInfotipProps extends PropsWithChildren {
58
58
  }
59
59
  declare const WarningInfotip: React$1.ForwardRefExoticComponent<WarningInfotipProps & React$1.RefAttributes<unknown>>;
60
60
 
61
+ declare const GlobalDialog: () => React$1.JSX.Element | null;
62
+
63
+ type DialogContent = {
64
+ component: ReactElement;
65
+ };
66
+ declare const openDialog: ({ component }: DialogContent) => void;
67
+ declare const closeDialog: () => void;
68
+
61
69
  type PopoverBodyProps = PropsWithChildren<{
62
70
  height?: number | 'auto';
63
71
  width?: number;
@@ -108,6 +116,32 @@ type Props = {
108
116
  };
109
117
  declare const PopoverSearch: ({ value, onSearch, placeholder }: Props) => React$1.JSX.Element;
110
118
 
119
+ declare const SaveChangesDialog: {
120
+ ({ children, onClose }: Pick<DialogProps, "children" | "onClose">): React$1.JSX.Element;
121
+ Title: ({ children, onClose }: React$1.PropsWithChildren & {
122
+ onClose?: () => void;
123
+ }) => React$1.JSX.Element;
124
+ Content: ({ children }: React$1.PropsWithChildren) => React$1.JSX.Element;
125
+ ContentText: (props: DialogContentTextProps) => React$1.JSX.Element;
126
+ Actions: ({ actions }: ConfirmationDialogActionsProps) => React$1.JSX.Element;
127
+ };
128
+ type Action = {
129
+ label: string;
130
+ action: () => void | Promise<void>;
131
+ };
132
+ type ConfirmationDialogActionsProps = {
133
+ actions: {
134
+ cancel?: Action;
135
+ confirm: Action;
136
+ discard?: Action;
137
+ };
138
+ };
139
+ declare const useDialog: () => {
140
+ isOpen: boolean;
141
+ open: () => void;
142
+ close: () => void;
143
+ };
144
+
111
145
  type UseEditableParams = {
112
146
  value: string;
113
147
  onSubmit: (value: string) => unknown;
@@ -134,4 +168,4 @@ declare const useEditable: ({ value, onSubmit, validation, onClick, onError }: U
134
168
  };
135
169
  };
136
170
 
137
- export { EditableField, EllipsisWithTooltip, ITEM_HEIGHT, InfoAlert, InfoTipCard, IntroductionModal, MenuItemInfotip, MenuListItem, PopoverBody, PopoverHeader, PopoverMenuList, type PopoverMenuListProps, PopoverSearch, StyledMenuList, ThemeProvider, type VirtualizedItem, WarningInfotip, useEditable };
171
+ export { EditableField, EllipsisWithTooltip, GlobalDialog, ITEM_HEIGHT, InfoAlert, InfoTipCard, IntroductionModal, MenuItemInfotip, MenuListItem, PopoverBody, PopoverHeader, PopoverMenuList, type PopoverMenuListProps, PopoverSearch, SaveChangesDialog, StyledMenuList, ThemeProvider, type VirtualizedItem, WarningInfotip, closeDialog, openDialog, useDialog, useEditable };
package/dist/index.js CHANGED
@@ -32,6 +32,7 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  EditableField: () => EditableField,
34
34
  EllipsisWithTooltip: () => EllipsisWithTooltip,
35
+ GlobalDialog: () => GlobalDialog,
35
36
  ITEM_HEIGHT: () => ITEM_HEIGHT,
36
37
  InfoAlert: () => InfoAlert,
37
38
  InfoTipCard: () => InfoTipCard,
@@ -42,9 +43,13 @@ __export(index_exports, {
42
43
  PopoverHeader: () => PopoverHeader,
43
44
  PopoverMenuList: () => PopoverMenuList,
44
45
  PopoverSearch: () => PopoverSearch,
46
+ SaveChangesDialog: () => SaveChangesDialog,
45
47
  StyledMenuList: () => StyledMenuList,
46
48
  ThemeProvider: () => ThemeProvider,
47
49
  WarningInfotip: () => WarningInfotip,
50
+ closeDialog: () => closeDialog,
51
+ openDialog: () => openDialog,
52
+ useDialog: () => useDialog,
48
53
  useEditable: () => useEditable
49
54
  });
50
55
  module.exports = __toCommonJS(index_exports);
@@ -291,15 +296,55 @@ var WarningInfotip = (0, import_react6.forwardRef)(
291
296
  }
292
297
  );
293
298
 
294
- // src/components/popover/body.tsx
299
+ // src/components/global-dialog/components/global-dialog.tsx
300
+ var import_react7 = require("react");
295
301
  var React9 = __toESM(require("react"));
296
302
  var import_ui9 = require("@elementor/ui");
303
+
304
+ // src/components/global-dialog/subscribers.ts
305
+ var currentDialogState = null;
306
+ var stateSubscribers = /* @__PURE__ */ new Set();
307
+ var subscribeToDialogState = (callback) => {
308
+ stateSubscribers.add(callback);
309
+ callback(currentDialogState);
310
+ return () => stateSubscribers.delete(callback);
311
+ };
312
+ var notifySubscribers = () => {
313
+ stateSubscribers.forEach((callback) => callback(currentDialogState));
314
+ };
315
+ var openDialog = ({ component }) => {
316
+ currentDialogState = { component };
317
+ notifySubscribers();
318
+ };
319
+ var closeDialog = () => {
320
+ currentDialogState = null;
321
+ notifySubscribers();
322
+ };
323
+
324
+ // src/components/global-dialog/components/global-dialog.tsx
325
+ var GlobalDialog = () => {
326
+ const [content, setContent] = (0, import_react7.useState)(null);
327
+ (0, import_react7.useEffect)(() => {
328
+ const unsubscribe = subscribeToDialogState(setContent);
329
+ return () => {
330
+ unsubscribe();
331
+ };
332
+ }, []);
333
+ if (!content) {
334
+ return null;
335
+ }
336
+ return /* @__PURE__ */ React9.createElement(ThemeProvider, null, /* @__PURE__ */ React9.createElement(import_ui9.Dialog, { role: "dialog", open: true, onClose: closeDialog, maxWidth: "sm", fullWidth: true }, content.component));
337
+ };
338
+
339
+ // src/components/popover/body.tsx
340
+ var React10 = __toESM(require("react"));
341
+ var import_ui10 = require("@elementor/ui");
297
342
  var SECTION_PADDING_INLINE = 32;
298
343
  var DEFAULT_POPOVER_HEIGHT = 348;
299
344
  var FALLBACK_POPOVER_WIDTH = 220;
300
345
  var PopoverBody = ({ children, height = DEFAULT_POPOVER_HEIGHT, width }) => {
301
- return /* @__PURE__ */ React9.createElement(
302
- import_ui9.Box,
346
+ return /* @__PURE__ */ React10.createElement(
347
+ import_ui10.Box,
303
348
  {
304
349
  display: "flex",
305
350
  flexDirection: "column",
@@ -315,8 +360,8 @@ var PopoverBody = ({ children, height = DEFAULT_POPOVER_HEIGHT, width }) => {
315
360
  };
316
361
 
317
362
  // src/components/popover/header.tsx
318
- var React10 = __toESM(require("react"));
319
- var import_ui10 = require("@elementor/ui");
363
+ var React11 = __toESM(require("react"));
364
+ var import_ui11 = require("@elementor/ui");
320
365
  var SIZE = "tiny";
321
366
  var PopoverHeader = ({ title, onClose, icon, actions }) => {
322
367
  const paddingAndSizing = {
@@ -325,23 +370,23 @@ var PopoverHeader = ({ title, onClose, icon, actions }) => {
325
370
  py: 1.5,
326
371
  maxHeight: 36
327
372
  };
328
- return /* @__PURE__ */ React10.createElement(import_ui10.Stack, { direction: "row", alignItems: "center", ...paddingAndSizing, sx: { columnGap: 0.5 } }, icon, /* @__PURE__ */ React10.createElement(import_ui10.Typography, { variant: "subtitle2", sx: { fontSize: "12px", mt: 0.25 } }, title), /* @__PURE__ */ React10.createElement(import_ui10.Stack, { direction: "row", sx: { ml: "auto" } }, actions, /* @__PURE__ */ React10.createElement(import_ui10.CloseButton, { slotProps: { icon: { fontSize: SIZE } }, sx: { ml: "auto" }, onClick: onClose })));
373
+ return /* @__PURE__ */ React11.createElement(import_ui11.Stack, { direction: "row", alignItems: "center", ...paddingAndSizing, sx: { columnGap: 0.5 } }, icon, /* @__PURE__ */ React11.createElement(import_ui11.Typography, { variant: "subtitle2", sx: { fontSize: "12px", mt: 0.25 } }, title), /* @__PURE__ */ React11.createElement(import_ui11.Stack, { direction: "row", sx: { ml: "auto" } }, actions, /* @__PURE__ */ React11.createElement(import_ui11.CloseButton, { slotProps: { icon: { fontSize: SIZE } }, sx: { ml: "auto" }, onClick: onClose })));
329
374
  };
330
375
 
331
376
  // src/components/popover/menu-list.tsx
332
- var React11 = __toESM(require("react"));
333
- var import_react9 = require("react");
334
- var import_ui11 = require("@elementor/ui");
377
+ var React12 = __toESM(require("react"));
378
+ var import_react10 = require("react");
379
+ var import_ui12 = require("@elementor/ui");
335
380
  var import_react_virtual = require("@tanstack/react-virtual");
336
381
 
337
382
  // src/hooks/use-scroll-to-selected.ts
338
- var import_react7 = require("react");
383
+ var import_react8 = require("react");
339
384
  var useScrollToSelected = ({
340
385
  selectedValue,
341
386
  items,
342
387
  virtualizer
343
388
  }) => {
344
- (0, import_react7.useEffect)(() => {
389
+ (0, import_react8.useEffect)(() => {
345
390
  if (!selectedValue || items.length === 0) {
346
391
  return;
347
392
  }
@@ -353,10 +398,10 @@ var useScrollToSelected = ({
353
398
  };
354
399
 
355
400
  // src/hooks/use-scroll-top.ts
356
- var import_react8 = require("react");
401
+ var import_react9 = require("react");
357
402
  var useScrollTop = ({ containerRef }) => {
358
- const [scrollTop, setScrollTop] = (0, import_react8.useState)(0);
359
- (0, import_react8.useEffect)(() => {
403
+ const [scrollTop, setScrollTop] = (0, import_react9.useState)(0);
404
+ (0, import_react9.useEffect)(() => {
360
405
  const container = containerRef.current;
361
406
  if (!container) {
362
407
  return;
@@ -395,10 +440,10 @@ var PopoverMenuList = ({
395
440
  noResultsComponent,
396
441
  menuListTemplate: CustomMenuList
397
442
  }) => {
398
- const containerRef = (0, import_react9.useRef)(null);
443
+ const containerRef = (0, import_react10.useRef)(null);
399
444
  const scrollTop = useScrollTop({ containerRef });
400
445
  const MenuListComponent = CustomMenuList || StyledMenuList;
401
- const stickyIndices = (0, import_react9.useMemo)(
446
+ const stickyIndices = (0, import_react10.useMemo)(
402
447
  () => items.reduce((categoryIndices, item, index) => {
403
448
  if (item.type === "category") {
404
449
  categoryIndices.push(index);
@@ -430,7 +475,7 @@ var PopoverMenuList = ({
430
475
  });
431
476
  useScrollToSelected({ selectedValue, items, virtualizer });
432
477
  const virtualItems = virtualizer.getVirtualItems();
433
- return /* @__PURE__ */ React11.createElement(import_ui11.Box, { ref: containerRef, sx: { height: "100%", overflowY: "auto" } }, items.length === 0 && noResultsComponent ? noResultsComponent : /* @__PURE__ */ React11.createElement(
478
+ return /* @__PURE__ */ React12.createElement(import_ui12.Box, { ref: containerRef, sx: { height: "100%", overflowY: "auto" } }, items.length === 0 && noResultsComponent ? noResultsComponent : /* @__PURE__ */ React12.createElement(
434
479
  MenuListComponent,
435
480
  {
436
481
  role: "listbox",
@@ -448,8 +493,8 @@ var PopoverMenuList = ({
448
493
  }
449
494
  if (item.type === "category") {
450
495
  const shouldStick = virtualRow.start + MENU_LIST_PADDING_TOP <= scrollTop;
451
- return /* @__PURE__ */ React11.createElement(
452
- import_ui11.MenuSubheader,
496
+ return /* @__PURE__ */ React12.createElement(
497
+ import_ui12.MenuSubheader,
453
498
  {
454
499
  key: virtualRow.key,
455
500
  style: shouldStick ? {} : menuSubHeaderAbsoluteStyling(virtualRow.start),
@@ -459,8 +504,8 @@ var PopoverMenuList = ({
459
504
  );
460
505
  }
461
506
  const isDisabled = item.disabled;
462
- return /* @__PURE__ */ React11.createElement(
463
- import_ui11.ListItem,
507
+ return /* @__PURE__ */ React12.createElement(
508
+ import_ui12.ListItem,
464
509
  {
465
510
  key: virtualRow.key,
466
511
  role: "option",
@@ -498,7 +543,7 @@ var PopoverMenuList = ({
498
543
  })
499
544
  ));
500
545
  };
501
- var StyledMenuList = (0, import_ui11.styled)(import_ui11.MenuList)(({ theme }) => ({
546
+ var StyledMenuList = (0, import_ui12.styled)(import_ui12.MenuList)(({ theme }) => ({
502
547
  "& > li": {
503
548
  height: ITEM_HEIGHT,
504
549
  width: "100%",
@@ -529,14 +574,14 @@ var StyledMenuList = (0, import_ui11.styled)(import_ui11.MenuList)(({ theme }) =
529
574
  }));
530
575
 
531
576
  // src/components/popover/search.tsx
532
- var React12 = __toESM(require("react"));
533
- var import_react10 = require("react");
577
+ var React13 = __toESM(require("react"));
578
+ var import_react11 = require("react");
534
579
  var import_icons2 = require("@elementor/icons");
535
- var import_ui12 = require("@elementor/ui");
580
+ var import_ui13 = require("@elementor/ui");
536
581
  var import_i18n2 = require("@wordpress/i18n");
537
582
  var SIZE2 = "tiny";
538
583
  var PopoverSearch = ({ value, onSearch, placeholder }) => {
539
- const inputRef = (0, import_react10.useRef)(null);
584
+ const inputRef = (0, import_react11.useRef)(null);
540
585
  const handleClear = () => {
541
586
  onSearch("");
542
587
  inputRef.current?.focus();
@@ -544,8 +589,8 @@ var PopoverSearch = ({ value, onSearch, placeholder }) => {
544
589
  const handleInputChange = (event) => {
545
590
  onSearch(event.target.value);
546
591
  };
547
- return /* @__PURE__ */ React12.createElement(import_ui12.Box, { sx: { px: 2, pb: 1.5 } }, /* @__PURE__ */ React12.createElement(
548
- import_ui12.TextField,
592
+ return /* @__PURE__ */ React13.createElement(import_ui13.Box, { sx: { px: 2, pb: 1.5 } }, /* @__PURE__ */ React13.createElement(
593
+ import_ui13.TextField,
549
594
  {
550
595
  autoFocus: true,
551
596
  fullWidth: true,
@@ -555,18 +600,60 @@ var PopoverSearch = ({ value, onSearch, placeholder }) => {
555
600
  onChange: handleInputChange,
556
601
  placeholder,
557
602
  InputProps: {
558
- startAdornment: /* @__PURE__ */ React12.createElement(import_ui12.InputAdornment, { position: "start" }, /* @__PURE__ */ React12.createElement(import_icons2.SearchIcon, { fontSize: SIZE2 })),
559
- endAdornment: value && /* @__PURE__ */ React12.createElement(import_ui12.IconButton, { size: SIZE2, onClick: handleClear, "aria-label": (0, import_i18n2.__)("Clear", "elementor") }, /* @__PURE__ */ React12.createElement(import_icons2.XIcon, { color: "action", fontSize: SIZE2 }))
603
+ startAdornment: /* @__PURE__ */ React13.createElement(import_ui13.InputAdornment, { position: "start" }, /* @__PURE__ */ React13.createElement(import_icons2.SearchIcon, { fontSize: SIZE2 })),
604
+ endAdornment: value && /* @__PURE__ */ React13.createElement(import_ui13.IconButton, { size: SIZE2, onClick: handleClear, "aria-label": (0, import_i18n2.__)("Clear", "elementor") }, /* @__PURE__ */ React13.createElement(import_icons2.XIcon, { color: "action", fontSize: SIZE2 }))
560
605
  }
561
606
  }
562
607
  ));
563
608
  };
564
609
 
610
+ // src/components/save-changes-dialog.tsx
611
+ var React14 = __toESM(require("react"));
612
+ var import_react12 = require("react");
613
+ var import_icons3 = require("@elementor/icons");
614
+ var import_ui14 = require("@elementor/ui");
615
+ var TITLE_ID = "save-changes-dialog";
616
+ var SaveChangesDialog = ({ children, onClose }) => /* @__PURE__ */ React14.createElement(import_ui14.Dialog, { open: true, onClose, "aria-labelledby": TITLE_ID, maxWidth: "xs" }, children);
617
+ var SaveChangesDialogTitle = ({ children, onClose }) => /* @__PURE__ */ React14.createElement(
618
+ import_ui14.DialogTitle,
619
+ {
620
+ id: TITLE_ID,
621
+ display: "flex",
622
+ alignItems: "center",
623
+ gap: 1,
624
+ sx: { lineHeight: 1, justifyContent: "space-between" }
625
+ },
626
+ /* @__PURE__ */ React14.createElement(import_ui14.Stack, { direction: "row", alignItems: "center", gap: 1 }, /* @__PURE__ */ React14.createElement(import_icons3.AlertTriangleFilledIcon, { color: "secondary" }), children),
627
+ onClose && /* @__PURE__ */ React14.createElement(import_ui14.IconButton, { onClick: onClose, size: "small" }, /* @__PURE__ */ React14.createElement(import_icons3.XIcon, null))
628
+ );
629
+ var SaveChangesDialogContent = ({ children }) => /* @__PURE__ */ React14.createElement(import_ui14.DialogContent, null, children);
630
+ var SaveChangesDialogContentText = (props) => /* @__PURE__ */ React14.createElement(import_ui14.DialogContentText, { variant: "body2", color: "textPrimary", display: "flex", flexDirection: "column", ...props });
631
+ var SaveChangesDialogActions = ({ actions }) => {
632
+ const [isConfirming, setIsConfirming] = (0, import_react12.useState)(false);
633
+ const { cancel, confirm, discard } = actions;
634
+ const onConfirm = async () => {
635
+ setIsConfirming(true);
636
+ await confirm.action();
637
+ setIsConfirming(false);
638
+ };
639
+ return /* @__PURE__ */ React14.createElement(import_ui14.DialogActions, null, cancel && /* @__PURE__ */ React14.createElement(import_ui14.Button, { variant: "text", color: "secondary", onClick: cancel.action }, cancel.label), discard && /* @__PURE__ */ React14.createElement(import_ui14.Button, { variant: "text", color: "secondary", onClick: discard.action }, discard.label), /* @__PURE__ */ React14.createElement(import_ui14.Button, { variant: "contained", color: "secondary", onClick: onConfirm, loading: isConfirming }, confirm.label));
640
+ };
641
+ SaveChangesDialog.Title = SaveChangesDialogTitle;
642
+ SaveChangesDialog.Content = SaveChangesDialogContent;
643
+ SaveChangesDialog.ContentText = SaveChangesDialogContentText;
644
+ SaveChangesDialog.Actions = SaveChangesDialogActions;
645
+ var useDialog = () => {
646
+ const [isOpen, setIsOpen] = (0, import_react12.useState)(false);
647
+ const open = () => setIsOpen(true);
648
+ const close = () => setIsOpen(false);
649
+ return { isOpen, open, close };
650
+ };
651
+
565
652
  // src/hooks/use-editable.ts
566
- var import_react11 = require("react");
653
+ var import_react13 = require("react");
567
654
  var useEditable = ({ value, onSubmit, validation, onClick, onError }) => {
568
- const [isEditing, setIsEditing] = (0, import_react11.useState)(false);
569
- const [error, setError] = (0, import_react11.useState)(null);
655
+ const [isEditing, setIsEditing] = (0, import_react13.useState)(false);
656
+ const [error, setError] = (0, import_react13.useState)(null);
570
657
  const ref = useSelection(isEditing);
571
658
  const isDirty = (newValue) => newValue !== value;
572
659
  const openEditMode = () => {
@@ -639,8 +726,8 @@ var useEditable = ({ value, onSubmit, validation, onClick, onError }) => {
639
726
  };
640
727
  };
641
728
  var useSelection = (isEditing) => {
642
- const ref = (0, import_react11.useRef)(null);
643
- (0, import_react11.useEffect)(() => {
729
+ const ref = (0, import_react13.useRef)(null);
730
+ (0, import_react13.useEffect)(() => {
644
731
  if (isEditing) {
645
732
  selectAll(ref.current);
646
733
  }
@@ -661,6 +748,7 @@ var selectAll = (el) => {
661
748
  0 && (module.exports = {
662
749
  EditableField,
663
750
  EllipsisWithTooltip,
751
+ GlobalDialog,
664
752
  ITEM_HEIGHT,
665
753
  InfoAlert,
666
754
  InfoTipCard,
@@ -671,9 +759,13 @@ var selectAll = (el) => {
671
759
  PopoverHeader,
672
760
  PopoverMenuList,
673
761
  PopoverSearch,
762
+ SaveChangesDialog,
674
763
  StyledMenuList,
675
764
  ThemeProvider,
676
765
  WarningInfotip,
766
+ closeDialog,
767
+ openDialog,
768
+ useDialog,
677
769
  useEditable
678
770
  });
679
771
  //# sourceMappingURL=index.js.map
package/dist/index.mjs CHANGED
@@ -254,14 +254,54 @@ var WarningInfotip = forwardRef5(
254
254
  }
255
255
  );
256
256
 
257
- // src/components/popover/body.tsx
257
+ // src/components/global-dialog/components/global-dialog.tsx
258
+ import { useEffect as useEffect3, useState as useState4 } from "react";
258
259
  import * as React9 from "react";
260
+ import { Dialog as Dialog2 } from "@elementor/ui";
261
+
262
+ // src/components/global-dialog/subscribers.ts
263
+ var currentDialogState = null;
264
+ var stateSubscribers = /* @__PURE__ */ new Set();
265
+ var subscribeToDialogState = (callback) => {
266
+ stateSubscribers.add(callback);
267
+ callback(currentDialogState);
268
+ return () => stateSubscribers.delete(callback);
269
+ };
270
+ var notifySubscribers = () => {
271
+ stateSubscribers.forEach((callback) => callback(currentDialogState));
272
+ };
273
+ var openDialog = ({ component }) => {
274
+ currentDialogState = { component };
275
+ notifySubscribers();
276
+ };
277
+ var closeDialog = () => {
278
+ currentDialogState = null;
279
+ notifySubscribers();
280
+ };
281
+
282
+ // src/components/global-dialog/components/global-dialog.tsx
283
+ var GlobalDialog = () => {
284
+ const [content, setContent] = useState4(null);
285
+ useEffect3(() => {
286
+ const unsubscribe = subscribeToDialogState(setContent);
287
+ return () => {
288
+ unsubscribe();
289
+ };
290
+ }, []);
291
+ if (!content) {
292
+ return null;
293
+ }
294
+ return /* @__PURE__ */ React9.createElement(ThemeProvider, null, /* @__PURE__ */ React9.createElement(Dialog2, { role: "dialog", open: true, onClose: closeDialog, maxWidth: "sm", fullWidth: true }, content.component));
295
+ };
296
+
297
+ // src/components/popover/body.tsx
298
+ import * as React10 from "react";
259
299
  import { Box as Box4 } from "@elementor/ui";
260
300
  var SECTION_PADDING_INLINE = 32;
261
301
  var DEFAULT_POPOVER_HEIGHT = 348;
262
302
  var FALLBACK_POPOVER_WIDTH = 220;
263
303
  var PopoverBody = ({ children, height = DEFAULT_POPOVER_HEIGHT, width }) => {
264
- return /* @__PURE__ */ React9.createElement(
304
+ return /* @__PURE__ */ React10.createElement(
265
305
  Box4,
266
306
  {
267
307
  display: "flex",
@@ -278,7 +318,7 @@ var PopoverBody = ({ children, height = DEFAULT_POPOVER_HEIGHT, width }) => {
278
318
  };
279
319
 
280
320
  // src/components/popover/header.tsx
281
- import * as React10 from "react";
321
+ import * as React11 from "react";
282
322
  import { CloseButton, Stack, Typography as Typography3 } from "@elementor/ui";
283
323
  var SIZE = "tiny";
284
324
  var PopoverHeader = ({ title, onClose, icon, actions }) => {
@@ -288,23 +328,23 @@ var PopoverHeader = ({ title, onClose, icon, actions }) => {
288
328
  py: 1.5,
289
329
  maxHeight: 36
290
330
  };
291
- return /* @__PURE__ */ React10.createElement(Stack, { direction: "row", alignItems: "center", ...paddingAndSizing, sx: { columnGap: 0.5 } }, icon, /* @__PURE__ */ React10.createElement(Typography3, { variant: "subtitle2", sx: { fontSize: "12px", mt: 0.25 } }, title), /* @__PURE__ */ React10.createElement(Stack, { direction: "row", sx: { ml: "auto" } }, actions, /* @__PURE__ */ React10.createElement(CloseButton, { slotProps: { icon: { fontSize: SIZE } }, sx: { ml: "auto" }, onClick: onClose })));
331
+ return /* @__PURE__ */ React11.createElement(Stack, { direction: "row", alignItems: "center", ...paddingAndSizing, sx: { columnGap: 0.5 } }, icon, /* @__PURE__ */ React11.createElement(Typography3, { variant: "subtitle2", sx: { fontSize: "12px", mt: 0.25 } }, title), /* @__PURE__ */ React11.createElement(Stack, { direction: "row", sx: { ml: "auto" } }, actions, /* @__PURE__ */ React11.createElement(CloseButton, { slotProps: { icon: { fontSize: SIZE } }, sx: { ml: "auto" }, onClick: onClose })));
292
332
  };
293
333
 
294
334
  // src/components/popover/menu-list.tsx
295
- import * as React11 from "react";
335
+ import * as React12 from "react";
296
336
  import { useMemo, useRef } from "react";
297
337
  import { Box as Box5, ListItem, MenuList, MenuSubheader, styled as styled2 } from "@elementor/ui";
298
338
  import { useVirtualizer } from "@tanstack/react-virtual";
299
339
 
300
340
  // src/hooks/use-scroll-to-selected.ts
301
- import { useEffect as useEffect3 } from "react";
341
+ import { useEffect as useEffect4 } from "react";
302
342
  var useScrollToSelected = ({
303
343
  selectedValue,
304
344
  items,
305
345
  virtualizer
306
346
  }) => {
307
- useEffect3(() => {
347
+ useEffect4(() => {
308
348
  if (!selectedValue || items.length === 0) {
309
349
  return;
310
350
  }
@@ -316,10 +356,10 @@ var useScrollToSelected = ({
316
356
  };
317
357
 
318
358
  // src/hooks/use-scroll-top.ts
319
- import { useEffect as useEffect4, useState as useState4 } from "react";
359
+ import { useEffect as useEffect5, useState as useState5 } from "react";
320
360
  var useScrollTop = ({ containerRef }) => {
321
- const [scrollTop, setScrollTop] = useState4(0);
322
- useEffect4(() => {
361
+ const [scrollTop, setScrollTop] = useState5(0);
362
+ useEffect5(() => {
323
363
  const container = containerRef.current;
324
364
  if (!container) {
325
365
  return;
@@ -393,7 +433,7 @@ var PopoverMenuList = ({
393
433
  });
394
434
  useScrollToSelected({ selectedValue, items, virtualizer });
395
435
  const virtualItems = virtualizer.getVirtualItems();
396
- return /* @__PURE__ */ React11.createElement(Box5, { ref: containerRef, sx: { height: "100%", overflowY: "auto" } }, items.length === 0 && noResultsComponent ? noResultsComponent : /* @__PURE__ */ React11.createElement(
436
+ return /* @__PURE__ */ React12.createElement(Box5, { ref: containerRef, sx: { height: "100%", overflowY: "auto" } }, items.length === 0 && noResultsComponent ? noResultsComponent : /* @__PURE__ */ React12.createElement(
397
437
  MenuListComponent,
398
438
  {
399
439
  role: "listbox",
@@ -411,7 +451,7 @@ var PopoverMenuList = ({
411
451
  }
412
452
  if (item.type === "category") {
413
453
  const shouldStick = virtualRow.start + MENU_LIST_PADDING_TOP <= scrollTop;
414
- return /* @__PURE__ */ React11.createElement(
454
+ return /* @__PURE__ */ React12.createElement(
415
455
  MenuSubheader,
416
456
  {
417
457
  key: virtualRow.key,
@@ -422,7 +462,7 @@ var PopoverMenuList = ({
422
462
  );
423
463
  }
424
464
  const isDisabled = item.disabled;
425
- return /* @__PURE__ */ React11.createElement(
465
+ return /* @__PURE__ */ React12.createElement(
426
466
  ListItem,
427
467
  {
428
468
  key: virtualRow.key,
@@ -492,7 +532,7 @@ var StyledMenuList = styled2(MenuList)(({ theme }) => ({
492
532
  }));
493
533
 
494
534
  // src/components/popover/search.tsx
495
- import * as React12 from "react";
535
+ import * as React13 from "react";
496
536
  import { useRef as useRef2 } from "react";
497
537
  import { SearchIcon, XIcon } from "@elementor/icons";
498
538
  import { Box as Box6, IconButton, InputAdornment, TextField } from "@elementor/ui";
@@ -507,7 +547,7 @@ var PopoverSearch = ({ value, onSearch, placeholder }) => {
507
547
  const handleInputChange = (event) => {
508
548
  onSearch(event.target.value);
509
549
  };
510
- return /* @__PURE__ */ React12.createElement(Box6, { sx: { px: 2, pb: 1.5 } }, /* @__PURE__ */ React12.createElement(
550
+ return /* @__PURE__ */ React13.createElement(Box6, { sx: { px: 2, pb: 1.5 } }, /* @__PURE__ */ React13.createElement(
511
551
  TextField,
512
552
  {
513
553
  autoFocus: true,
@@ -518,18 +558,69 @@ var PopoverSearch = ({ value, onSearch, placeholder }) => {
518
558
  onChange: handleInputChange,
519
559
  placeholder,
520
560
  InputProps: {
521
- startAdornment: /* @__PURE__ */ React12.createElement(InputAdornment, { position: "start" }, /* @__PURE__ */ React12.createElement(SearchIcon, { fontSize: SIZE2 })),
522
- endAdornment: value && /* @__PURE__ */ React12.createElement(IconButton, { size: SIZE2, onClick: handleClear, "aria-label": __2("Clear", "elementor") }, /* @__PURE__ */ React12.createElement(XIcon, { color: "action", fontSize: SIZE2 }))
561
+ startAdornment: /* @__PURE__ */ React13.createElement(InputAdornment, { position: "start" }, /* @__PURE__ */ React13.createElement(SearchIcon, { fontSize: SIZE2 })),
562
+ endAdornment: value && /* @__PURE__ */ React13.createElement(IconButton, { size: SIZE2, onClick: handleClear, "aria-label": __2("Clear", "elementor") }, /* @__PURE__ */ React13.createElement(XIcon, { color: "action", fontSize: SIZE2 }))
523
563
  }
524
564
  }
525
565
  ));
526
566
  };
527
567
 
568
+ // src/components/save-changes-dialog.tsx
569
+ import * as React14 from "react";
570
+ import { useState as useState6 } from "react";
571
+ import { AlertTriangleFilledIcon, XIcon as XIcon2 } from "@elementor/icons";
572
+ import {
573
+ Button as Button3,
574
+ Dialog as Dialog3,
575
+ DialogActions as DialogActions2,
576
+ DialogContent,
577
+ DialogContentText,
578
+ DialogTitle as DialogTitle2,
579
+ IconButton as IconButton2,
580
+ Stack as Stack2
581
+ } from "@elementor/ui";
582
+ var TITLE_ID = "save-changes-dialog";
583
+ var SaveChangesDialog = ({ children, onClose }) => /* @__PURE__ */ React14.createElement(Dialog3, { open: true, onClose, "aria-labelledby": TITLE_ID, maxWidth: "xs" }, children);
584
+ var SaveChangesDialogTitle = ({ children, onClose }) => /* @__PURE__ */ React14.createElement(
585
+ DialogTitle2,
586
+ {
587
+ id: TITLE_ID,
588
+ display: "flex",
589
+ alignItems: "center",
590
+ gap: 1,
591
+ sx: { lineHeight: 1, justifyContent: "space-between" }
592
+ },
593
+ /* @__PURE__ */ React14.createElement(Stack2, { direction: "row", alignItems: "center", gap: 1 }, /* @__PURE__ */ React14.createElement(AlertTriangleFilledIcon, { color: "secondary" }), children),
594
+ onClose && /* @__PURE__ */ React14.createElement(IconButton2, { onClick: onClose, size: "small" }, /* @__PURE__ */ React14.createElement(XIcon2, null))
595
+ );
596
+ var SaveChangesDialogContent = ({ children }) => /* @__PURE__ */ React14.createElement(DialogContent, null, children);
597
+ var SaveChangesDialogContentText = (props) => /* @__PURE__ */ React14.createElement(DialogContentText, { variant: "body2", color: "textPrimary", display: "flex", flexDirection: "column", ...props });
598
+ var SaveChangesDialogActions = ({ actions }) => {
599
+ const [isConfirming, setIsConfirming] = useState6(false);
600
+ const { cancel, confirm, discard } = actions;
601
+ const onConfirm = async () => {
602
+ setIsConfirming(true);
603
+ await confirm.action();
604
+ setIsConfirming(false);
605
+ };
606
+ return /* @__PURE__ */ React14.createElement(DialogActions2, null, cancel && /* @__PURE__ */ React14.createElement(Button3, { variant: "text", color: "secondary", onClick: cancel.action }, cancel.label), discard && /* @__PURE__ */ React14.createElement(Button3, { variant: "text", color: "secondary", onClick: discard.action }, discard.label), /* @__PURE__ */ React14.createElement(Button3, { variant: "contained", color: "secondary", onClick: onConfirm, loading: isConfirming }, confirm.label));
607
+ };
608
+ SaveChangesDialog.Title = SaveChangesDialogTitle;
609
+ SaveChangesDialog.Content = SaveChangesDialogContent;
610
+ SaveChangesDialog.ContentText = SaveChangesDialogContentText;
611
+ SaveChangesDialog.Actions = SaveChangesDialogActions;
612
+ var useDialog = () => {
613
+ const [isOpen, setIsOpen] = useState6(false);
614
+ const open = () => setIsOpen(true);
615
+ const close = () => setIsOpen(false);
616
+ return { isOpen, open, close };
617
+ };
618
+
528
619
  // src/hooks/use-editable.ts
529
- import { useEffect as useEffect5, useRef as useRef3, useState as useState5 } from "react";
620
+ import { useEffect as useEffect6, useRef as useRef3, useState as useState7 } from "react";
530
621
  var useEditable = ({ value, onSubmit, validation, onClick, onError }) => {
531
- const [isEditing, setIsEditing] = useState5(false);
532
- const [error, setError] = useState5(null);
622
+ const [isEditing, setIsEditing] = useState7(false);
623
+ const [error, setError] = useState7(null);
533
624
  const ref = useSelection(isEditing);
534
625
  const isDirty = (newValue) => newValue !== value;
535
626
  const openEditMode = () => {
@@ -603,7 +694,7 @@ var useEditable = ({ value, onSubmit, validation, onClick, onError }) => {
603
694
  };
604
695
  var useSelection = (isEditing) => {
605
696
  const ref = useRef3(null);
606
- useEffect5(() => {
697
+ useEffect6(() => {
607
698
  if (isEditing) {
608
699
  selectAll(ref.current);
609
700
  }
@@ -623,6 +714,7 @@ var selectAll = (el) => {
623
714
  export {
624
715
  EditableField,
625
716
  EllipsisWithTooltip,
717
+ GlobalDialog,
626
718
  ITEM_HEIGHT,
627
719
  InfoAlert,
628
720
  InfoTipCard,
@@ -633,9 +725,13 @@ export {
633
725
  PopoverHeader,
634
726
  PopoverMenuList,
635
727
  PopoverSearch,
728
+ SaveChangesDialog,
636
729
  StyledMenuList,
637
730
  ThemeProvider,
638
731
  WarningInfotip,
732
+ closeDialog,
733
+ openDialog,
734
+ useDialog,
639
735
  useEditable
640
736
  };
641
737
  //# sourceMappingURL=index.mjs.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elementor/editor-ui",
3
3
  "description": "Elementor Editor UI",
4
- "version": "3.33.0-117",
4
+ "version": "3.33.0-119",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -37,7 +37,7 @@
37
37
  "react-dom": "^18.3.1"
38
38
  },
39
39
  "dependencies": {
40
- "@elementor/editor-v1-adapters": "3.33.0-117",
40
+ "@elementor/editor-v1-adapters": "3.33.0-119",
41
41
  "@elementor/icons": "1.46.0",
42
42
  "@elementor/ui": "1.36.12",
43
43
  "@tanstack/react-virtual": "^3.13.3",
@@ -0,0 +1,272 @@
1
+ import * as React from 'react';
2
+ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
3
+
4
+ import { GlobalDialog } from '../components/global-dialog';
5
+ import { closeDialog, type DialogStateCallback, openDialog } from '../subscribers';
6
+
7
+ // Get mock functions for cleanup
8
+ const mockEventBus = jest.requireMock( '../subscribers' );
9
+
10
+ jest.mock( '../subscribers', () => {
11
+ let currentState: { component: React.ReactElement } | null = null;
12
+ const subscribers = new Set< DialogStateCallback >();
13
+
14
+ const notifySubscribers = () => {
15
+ subscribers.forEach( ( callback ) => callback( currentState ) );
16
+ };
17
+
18
+ return {
19
+ subscribeToDialogState: jest.fn( ( callback: DialogStateCallback ) => {
20
+ subscribers.add( callback );
21
+ // Call callback immediately with current state (matches real implementation)
22
+ callback( currentState );
23
+ return () => subscribers.delete( callback );
24
+ } ),
25
+ openDialog: jest.fn( ( { component }: { component: React.ReactElement } ) => {
26
+ currentState = { component };
27
+ // Notify synchronously (matches real implementation)
28
+ notifySubscribers();
29
+ } ),
30
+ closeDialog: jest.fn( () => {
31
+ currentState = null;
32
+ // Notify synchronously (matches real implementation)
33
+ notifySubscribers();
34
+ } ),
35
+ // For manual control in tests
36
+ __notifySubscribers: notifySubscribers,
37
+ __getCurrentState: () => currentState,
38
+ __getSubscribers: () => subscribers,
39
+ __reset: () => {
40
+ currentState = null;
41
+ subscribers.clear();
42
+ },
43
+ };
44
+ } );
45
+
46
+ // Helper function to render GlobalDialog
47
+ const renderGlobalDialog = () => {
48
+ return render( <GlobalDialog /> );
49
+ };
50
+
51
+ describe( 'GlobalDialog', () => {
52
+ // Reset dialog state before each test to avoid test interference
53
+ beforeEach( () => {
54
+ mockEventBus.__reset();
55
+ } );
56
+
57
+ afterEach( () => {
58
+ // Clean up any open dialogs after each test
59
+ mockEventBus.__reset();
60
+ } );
61
+
62
+ it( 'should not render anything when no dialog is open', () => {
63
+ // Act
64
+ renderGlobalDialog();
65
+
66
+ // Assert
67
+ expect( screen.queryByRole( 'dialog' ) ).not.toBeInTheDocument();
68
+ } );
69
+
70
+ it( 'should render dialog when openDialog is called', async () => {
71
+ // Arrange
72
+ const TestDialogContent = () => <div>Test Dialog Content</div>;
73
+
74
+ renderGlobalDialog();
75
+
76
+ // Act
77
+ act( () => {
78
+ openDialog( { component: <TestDialogContent /> } );
79
+ } );
80
+
81
+ // Assert - Wait for the dialog to appear
82
+ await waitFor( () => {
83
+ expect( screen.getAllByRole( 'dialog' ) ).toHaveLength( 2 ); // MUI creates 2 dialog elements
84
+ } );
85
+ expect( screen.getByText( 'Test Dialog Content' ) ).toBeInTheDocument();
86
+ } );
87
+
88
+ it( 'should close dialog when closeDialog is called', async () => {
89
+ // Arrange
90
+ const TestDialogContent = () => <div>Test Dialog Content</div>;
91
+
92
+ renderGlobalDialog();
93
+
94
+ // Open dialog first
95
+ act( () => {
96
+ openDialog( { component: <TestDialogContent /> } );
97
+ } );
98
+
99
+ await waitFor( () => {
100
+ expect( screen.getAllByRole( 'dialog' ) ).toHaveLength( 2 );
101
+ } );
102
+
103
+ // Act
104
+ act( () => {
105
+ closeDialog();
106
+ } );
107
+
108
+ // Assert
109
+ await waitFor( () => {
110
+ expect( screen.queryAllByRole( 'dialog' ) ).toHaveLength( 0 );
111
+ } );
112
+ expect( screen.queryByText( 'Test Dialog Content' ) ).not.toBeInTheDocument();
113
+ } );
114
+
115
+ it( 'should close dialog when Dialog onClose is triggered', async () => {
116
+ // Arrange
117
+ const TestDialogContent = () => <div>Test Dialog Content</div>;
118
+
119
+ renderGlobalDialog();
120
+
121
+ // Open dialog first
122
+ act( () => {
123
+ openDialog( { component: <TestDialogContent /> } );
124
+ } );
125
+
126
+ await waitFor( () => {
127
+ expect( screen.getAllByRole( 'dialog' ) ).toHaveLength( 2 );
128
+ } );
129
+ expect( screen.getByText( 'Test Dialog Content' ) ).toBeInTheDocument();
130
+
131
+ // Act - Simulate Dialog's onClose being called (like when user clicks close button)
132
+ act( () => {
133
+ closeDialog();
134
+ } );
135
+
136
+ // Assert
137
+ await waitFor( () => {
138
+ expect( screen.queryAllByRole( 'dialog' ) ).toHaveLength( 0 );
139
+ } );
140
+ expect( screen.queryByText( 'Test Dialog Content' ) ).not.toBeInTheDocument();
141
+ } );
142
+
143
+ it( 'should handle null/undefined dialog content gracefully', () => {
144
+ // This test ensures the component doesn't crash with invalid content
145
+ renderGlobalDialog();
146
+
147
+ // No dialog should be rendered when content is null
148
+ expect( screen.queryByRole( 'dialog' ) ).not.toBeInTheDocument();
149
+ } );
150
+ it( 'should replace dialog content when a new dialog is opened', async () => {
151
+ // Arrange
152
+ const FirstDialogContent = () => <div>First Dialog</div>;
153
+ const SecondDialogContent = () => <div>Second Dialog</div>;
154
+
155
+ renderGlobalDialog();
156
+
157
+ // Open first dialog
158
+ act( () => {
159
+ openDialog( { component: <FirstDialogContent /> } );
160
+ } );
161
+
162
+ await waitFor( () => {
163
+ expect( screen.getByText( 'First Dialog' ) ).toBeInTheDocument();
164
+ } );
165
+
166
+ // Act - Open second dialog
167
+ act( () => {
168
+ openDialog( { component: <SecondDialogContent /> } );
169
+ } );
170
+
171
+ // Assert
172
+ await waitFor( () => {
173
+ expect( screen.queryByText( 'First Dialog' ) ).not.toBeInTheDocument();
174
+ } );
175
+ expect( screen.getByText( 'Second Dialog' ) ).toBeInTheDocument();
176
+ } );
177
+
178
+ it( 'should subscribe to dialog state on mount', async () => {
179
+ // Arrange
180
+ const TestDialogContent = () => <div>Test Content</div>;
181
+
182
+ // Act
183
+ renderGlobalDialog();
184
+
185
+ // The component should be subscribed, so opening a dialog should work
186
+ act( () => {
187
+ openDialog( { component: <TestDialogContent /> } );
188
+ } );
189
+
190
+ // Assert
191
+ await waitFor( () => {
192
+ expect( screen.getAllByRole( 'dialog' ) ).toHaveLength( 2 );
193
+ } );
194
+ } );
195
+
196
+ it( 'should clean up subscription when component unmounts', async () => {
197
+ // Arrange
198
+ const TestDialogContent = () => <div>Test Dialog</div>;
199
+
200
+ const view = render( <GlobalDialog /> );
201
+ const unmount = view.unmount;
202
+
203
+ // Open dialog to verify subscription is working
204
+ act( () => {
205
+ openDialog( { component: <TestDialogContent /> } );
206
+ } );
207
+
208
+ await waitFor( () => {
209
+ expect( screen.getAllByRole( 'dialog' ) ).toHaveLength( 2 );
210
+ } );
211
+
212
+ // Act - Unmount component
213
+ unmount();
214
+
215
+ // Try to open dialog again - should not affect the unmounted component
216
+ act( () => {
217
+ openDialog( { component: <TestDialogContent /> } );
218
+ } );
219
+
220
+ // Assert - No dialog should be rendered since component is unmounted
221
+ expect( screen.queryAllByRole( 'dialog' ) ).toHaveLength( 0 );
222
+ } );
223
+ it( 'should work with store dispatch patterns like error dialogs', async () => {
224
+ // Arrange - Simulate error dialog pattern from editor-global-classes
225
+ const ErrorDialogContent = ( {
226
+ modifiedLabels,
227
+ }: {
228
+ modifiedLabels: Array< { original: string; modified: string; id: string } >;
229
+ } ) => (
230
+ <div>
231
+ <h3>Duplicate Labels Found</h3>
232
+ <ul>
233
+ { modifiedLabels.map( ( label ) => (
234
+ <li key={ label.id }>
235
+ { label.original } → { label.modified }
236
+ </li>
237
+ ) ) }
238
+ </ul>
239
+ <button onClick={ () => closeDialog() }>Close</button>
240
+ </div>
241
+ );
242
+
243
+ const mockModifiedLabels = [
244
+ { original: 'MyClass', modified: 'DUP_MyClass', id: 'class-1' },
245
+ { original: 'Button', modified: 'DUP_Button', id: 'class-2' },
246
+ ];
247
+
248
+ renderGlobalDialog();
249
+
250
+ // Act - Simulate error dialog opening (like in show-error-dialog.tsx)
251
+ act( () => {
252
+ openDialog( {
253
+ component: <ErrorDialogContent modifiedLabels={ mockModifiedLabels } />,
254
+ } );
255
+ } );
256
+
257
+ // Assert
258
+ await waitFor( () => {
259
+ expect( screen.getAllByRole( 'dialog' ) ).toHaveLength( 2 );
260
+ } );
261
+ expect( screen.getByText( 'Duplicate Labels Found' ) ).toBeInTheDocument();
262
+ expect( screen.getByText( 'MyClass → DUP_MyClass' ) ).toBeInTheDocument();
263
+ expect( screen.getByText( 'Button → DUP_Button' ) ).toBeInTheDocument();
264
+
265
+ // Test closing
266
+ fireEvent.click( screen.getByRole( 'button', { name: 'Close' } ) );
267
+
268
+ await waitFor( () => {
269
+ expect( screen.queryAllByRole( 'dialog' ) ).toHaveLength( 0 );
270
+ } );
271
+ } );
272
+ } );
@@ -0,0 +1,90 @@
1
+ import * as React from 'react';
2
+ import { waitFor } from '@testing-library/react';
3
+
4
+ import { closeDialog, type DialogContent, openDialog, subscribeToDialogState } from '../subscribers';
5
+
6
+ describe( 'subscribers', () => {
7
+ // Clean up state after each test
8
+ afterEach( () => {
9
+ closeDialog();
10
+ } );
11
+
12
+ it( 'should update state when openDialog is called', async () => {
13
+ // Arrange
14
+ const callback = jest.fn();
15
+ const testComponent = React.createElement( 'div', { children: 'Test' } );
16
+ const dialogContent: DialogContent = { component: testComponent };
17
+
18
+ subscribeToDialogState( callback );
19
+ callback.mockClear(); // Clear the initial call
20
+
21
+ // Act
22
+ openDialog( dialogContent );
23
+
24
+ // Assert
25
+ await waitFor( () => {
26
+ expect( callback ).toHaveBeenCalledWith( { component: testComponent } );
27
+ } );
28
+ await waitFor( () => {
29
+ expect( callback ).toHaveBeenCalledTimes( 1 );
30
+ } );
31
+ } );
32
+
33
+ it( 'should reset state to null when closeDialog is called', async () => {
34
+ // Arrange
35
+ const callback = jest.fn();
36
+ const testComponent = React.createElement( 'div', { children: 'Test' } );
37
+
38
+ subscribeToDialogState( callback );
39
+ openDialog( { component: testComponent } );
40
+
41
+ // Wait for the openDialog to complete
42
+ await waitFor( () => {
43
+ expect( callback ).toHaveBeenCalledWith( { component: testComponent } );
44
+ } );
45
+
46
+ callback.mockClear(); // Clear previous calls
47
+
48
+ // Act
49
+ closeDialog();
50
+
51
+ // Assert
52
+ await waitFor( () => {
53
+ expect( callback ).toHaveBeenCalledWith( null );
54
+ } );
55
+ await waitFor( () => {
56
+ expect( callback ).toHaveBeenCalledTimes( 1 );
57
+ } );
58
+ } );
59
+
60
+ it( 'should call callback immediately with current state on subscription', () => {
61
+ // Arrange
62
+ const testComponent = React.createElement( 'div', { children: 'Test' } );
63
+ openDialog( { component: testComponent } );
64
+
65
+ const callback = jest.fn();
66
+
67
+ // Act
68
+ subscribeToDialogState( callback );
69
+
70
+ // Assert
71
+ expect( callback ).toHaveBeenCalledWith( { component: testComponent } );
72
+ expect( callback ).toHaveBeenCalledTimes( 1 );
73
+ } );
74
+
75
+ it( 'should unsubscribe callback when unsubscribe function is called', () => {
76
+ // Arrange
77
+ const callback = jest.fn();
78
+ const testComponent = React.createElement( 'div', { children: 'Test' } );
79
+
80
+ const unsubscribe = subscribeToDialogState( callback );
81
+ callback.mockClear(); // Clear initial call
82
+
83
+ // Act
84
+ unsubscribe();
85
+ openDialog( { component: testComponent } );
86
+
87
+ // Assert
88
+ expect( callback ).not.toHaveBeenCalled();
89
+ } );
90
+ } );
@@ -0,0 +1,30 @@
1
+ import { useEffect, useState } from 'react';
2
+ import * as React from 'react';
3
+ import { Dialog } from '@elementor/ui';
4
+
5
+ import ThemeProvider from '../../theme-provider';
6
+ import { closeDialog, subscribeToDialogState } from '../subscribers';
7
+ import { type DialogContent } from '../subscribers';
8
+
9
+ export const GlobalDialog = () => {
10
+ const [ content, setContent ] = useState< DialogContent | null >( null );
11
+
12
+ useEffect( () => {
13
+ const unsubscribe = subscribeToDialogState( setContent );
14
+ return () => {
15
+ unsubscribe();
16
+ };
17
+ }, [] );
18
+
19
+ if ( ! content ) {
20
+ return null;
21
+ }
22
+
23
+ return (
24
+ <ThemeProvider>
25
+ <Dialog role="dialog" open onClose={ closeDialog } maxWidth="sm" fullWidth>
26
+ { content.component }
27
+ </Dialog>
28
+ </ThemeProvider>
29
+ );
30
+ };
@@ -0,0 +1,2 @@
1
+ export { GlobalDialog } from './components/global-dialog';
2
+ export { openDialog, closeDialog } from './subscribers';
@@ -0,0 +1,36 @@
1
+ import { type ReactElement } from 'react';
2
+
3
+ type DialogState = {
4
+ component: ReactElement;
5
+ } | null;
6
+
7
+ export type DialogStateCallback = ( state: DialogState ) => void;
8
+
9
+ let currentDialogState: DialogState = null;
10
+
11
+ const stateSubscribers = new Set< DialogStateCallback >();
12
+
13
+ export const subscribeToDialogState = ( callback: DialogStateCallback ) => {
14
+ stateSubscribers.add( callback );
15
+
16
+ callback( currentDialogState );
17
+ return () => stateSubscribers.delete( callback );
18
+ };
19
+
20
+ const notifySubscribers = () => {
21
+ stateSubscribers.forEach( ( callback ) => callback( currentDialogState ) );
22
+ };
23
+
24
+ export type DialogContent = {
25
+ component: ReactElement;
26
+ };
27
+
28
+ export const openDialog = ( { component }: DialogContent ) => {
29
+ currentDialogState = { component };
30
+ notifySubscribers();
31
+ };
32
+
33
+ export const closeDialog = () => {
34
+ currentDialogState = null;
35
+ notifySubscribers();
36
+ };
@@ -0,0 +1,106 @@
1
+ import * as React from 'react';
2
+ import { useState } from 'react';
3
+ import { AlertTriangleFilledIcon, XIcon } from '@elementor/icons';
4
+ import {
5
+ Button,
6
+ Dialog,
7
+ DialogActions,
8
+ DialogContent,
9
+ DialogContentText,
10
+ type DialogContentTextProps,
11
+ type DialogProps,
12
+ DialogTitle,
13
+ IconButton,
14
+ Stack,
15
+ } from '@elementor/ui';
16
+
17
+ const TITLE_ID = 'save-changes-dialog';
18
+
19
+ export const SaveChangesDialog = ( { children, onClose }: Pick< DialogProps, 'children' | 'onClose' > ) => (
20
+ <Dialog open onClose={ onClose } aria-labelledby={ TITLE_ID } maxWidth="xs">
21
+ { children }
22
+ </Dialog>
23
+ );
24
+
25
+ const SaveChangesDialogTitle = ( { children, onClose }: React.PropsWithChildren & { onClose?: () => void } ) => (
26
+ <DialogTitle
27
+ id={ TITLE_ID }
28
+ display="flex"
29
+ alignItems="center"
30
+ gap={ 1 }
31
+ sx={ { lineHeight: 1, justifyContent: 'space-between' } }
32
+ >
33
+ <Stack direction="row" alignItems="center" gap={ 1 }>
34
+ <AlertTriangleFilledIcon color="secondary" />
35
+ { children }
36
+ </Stack>
37
+ { onClose && (
38
+ <IconButton onClick={ onClose } size="small">
39
+ <XIcon />
40
+ </IconButton>
41
+ ) }
42
+ </DialogTitle>
43
+ );
44
+
45
+ const SaveChangesDialogContent = ( { children }: React.PropsWithChildren ) => (
46
+ <DialogContent>{ children }</DialogContent>
47
+ );
48
+
49
+ const SaveChangesDialogContentText = ( props: DialogContentTextProps ) => (
50
+ <DialogContentText variant="body2" color="textPrimary" display="flex" flexDirection="column" { ...props } />
51
+ );
52
+
53
+ type Action = {
54
+ label: string;
55
+ action: () => void | Promise< void >;
56
+ };
57
+
58
+ type ConfirmationDialogActionsProps = {
59
+ actions: {
60
+ cancel?: Action;
61
+ confirm: Action;
62
+ discard?: Action;
63
+ };
64
+ };
65
+
66
+ const SaveChangesDialogActions = ( { actions }: ConfirmationDialogActionsProps ) => {
67
+ const [ isConfirming, setIsConfirming ] = useState( false );
68
+ const { cancel, confirm, discard } = actions;
69
+
70
+ const onConfirm = async () => {
71
+ setIsConfirming( true );
72
+ await confirm.action();
73
+ setIsConfirming( false );
74
+ };
75
+ return (
76
+ <DialogActions>
77
+ { cancel && (
78
+ <Button variant="text" color="secondary" onClick={ cancel.action }>
79
+ { cancel.label }
80
+ </Button>
81
+ ) }
82
+ { discard && (
83
+ <Button variant="text" color="secondary" onClick={ discard.action }>
84
+ { discard.label }
85
+ </Button>
86
+ ) }
87
+ <Button variant="contained" color="secondary" onClick={ onConfirm } loading={ isConfirming }>
88
+ { confirm.label }
89
+ </Button>
90
+ </DialogActions>
91
+ );
92
+ };
93
+
94
+ SaveChangesDialog.Title = SaveChangesDialogTitle;
95
+ SaveChangesDialog.Content = SaveChangesDialogContent;
96
+ SaveChangesDialog.ContentText = SaveChangesDialogContentText;
97
+ SaveChangesDialog.Actions = SaveChangesDialogActions;
98
+
99
+ export const useDialog = () => {
100
+ const [ isOpen, setIsOpen ] = useState( false );
101
+
102
+ const open = () => setIsOpen( true );
103
+ const close = () => setIsOpen( false );
104
+
105
+ return { isOpen, open, close };
106
+ };
package/src/index.ts CHANGED
@@ -7,7 +7,9 @@ export { MenuListItem, MenuItemInfotip } from './components/menu-item';
7
7
  export { InfoTipCard } from './components/infotip-card';
8
8
  export { InfoAlert } from './components/info-alert';
9
9
  export { WarningInfotip } from './components/warning-infotip';
10
+ export { GlobalDialog, openDialog, closeDialog } from './components/global-dialog';
10
11
  export * from './components/popover';
12
+ export * from './components/save-changes-dialog';
11
13
 
12
14
  // hooks
13
15
  export { useEditable } from './hooks/use-editable';