Bin
2025-12-17 d616898802dfe7e5dd648bcf53c6d1f86b6d3642
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
import { createRef, type ReactElement } from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { cnb as cn } from "@humansignal/core/lib/utils/bem";
import { Button, type ButtonProps } from "../button/button";
import { Modal, type ModalProps } from "./ModalPopup";
import { ToastViewport } from "../toast/toast";
 
export type ExtraProps = {
  unique?: string;
  simple?: boolean;
  onHidden?: () => void;
  /**
   * Optional providers to wrap the modal with.
   * Allows applications to inject their own providers (ApiProvider, AuthProvider, etc.)
   */
  providers?: ReactElement[];
};
 
export type ConfirmProps<T = unknown> = ModalProps<T> & {
  okText?: string;
  onOk?: () => void;
  cancelText?: string;
  onCancel?: () => void;
  buttonLook?: ButtonProps["variant"];
} & ExtraProps;
 
export type InfoProps<T> = ModalProps<T> & {
  okText?: string;
  onOkPress?: () => void;
} & ExtraProps;
 
export type ModalUpdate<T, Props extends ModalProps<T> & ExtraProps> = {
  update: (newProps: Partial<Props>) => void;
  close: () => Promise<void> | undefined;
  visible: boolean;
};
 
/**
 * Creates a standalone modal with optional provider wrapping.
 *
 * @example
 * // Simple modal without providers
 * const modal = standaloneModal({ title: 'Hello', body: 'World', simple: true });
 *
 * @example
 * // Modal with app providers
 * const modal = standaloneModal({
 *   title: 'Hello',
 *   body: 'World',
 *   providers: [
 *     <ApiProvider key="api" />,
 *     <AuthProvider key="auth" />
 *   ]
 * });
 */
 
export type ModalUpdateProps<T> = ModalUpdate<T, ModalProps<T> & ExtraProps>;
const UNIQUE_MODALS = new Map<string, ModalUpdate<unknown, ModalProps<unknown> & ExtraProps>>();
 
const standaloneModal = <T,>({ simple = true, ...props }: ModalProps<T> & ExtraProps): ModalUpdateProps<T> => {
  if (props.unique && UNIQUE_MODALS.has(props.unique)) {
    return UNIQUE_MODALS.get(props.unique) as ModalUpdate<T, ModalProps<T> & ExtraProps>;
  }
  const modalRef = createRef<Modal>();
  const rootDiv = document.createElement("div");
  let renderCount = 0;
 
  rootDiv.className = cn("modal-holder").toClassName();
 
  document.body.appendChild(rootDiv);
 
  const renderModal = (props: ModalProps<T> & ExtraProps, animate?: boolean) => {
    renderCount++;
 
    // Get providers from props or use empty array for simple modals
    const providers = simple ? [] : (props.providers ?? []);
 
    // Check if ToastProvider is in the providers list
    const hasToastProvider = providers.some(
      (provider) => provider?.type?.name === "ToastProvider" || provider?.key === "toast",
    );
 
    // If providers are provided, wrap the modal with a MultiProvider-like structure
    const wrapWithProviders = (content: ReactElement) => {
      if (providers.length === 0) {
        return content;
      }
 
      // Nest providers from right to left (innermost to outermost)
      return providers.reduceRight((acc, provider) => {
        // Clone the provider and add the accumulated content as children
        return { ...provider, props: { ...provider.props, children: acc } };
      }, content);
    };
 
    const wrappedContent = wrapWithProviders(
      <>
        <Modal
          ref={modalRef}
          {...props}
          onHide={() => {
            props.onHidden?.();
            unmountComponentAtNode(rootDiv);
            rootDiv.remove();
            if (props.unique) UNIQUE_MODALS.delete(props.unique);
          }}
          animateAppearance={animate}
        />
        {hasToastProvider && <ToastViewport />}
      </>,
    );
 
    render(wrappedContent, rootDiv);
  };
 
  renderModal(props, true);
 
  const modalControls: ModalUpdate<T, ModalProps<T>> = {
    update(newProps: ModalProps<T>) {
      renderModal({ ...props, ...(newProps ?? {}), visible: true }, false);
    },
    close() {
      return modalRef.current?.hide();
    },
    get visible() {
      return modalRef.current?.visible ?? false;
    },
  };
 
  if (props.unique) {
    UNIQUE_MODALS.set(props.unique, modalControls as unknown as ModalUpdate<unknown, ModalProps<unknown> & ExtraProps>);
  }
 
  return modalControls;
};
 
/**
 * Creates a confirmation modal with OK and Cancel buttons.
 */
export const confirm = <T,>({ okText, onOk, cancelText, onCancel, buttonLook, ...props }: ConfirmProps<T>) => {
  const modal = standaloneModal({
    ...props,
    allowClose: false,
    footer: (
      <div className="flex gap-2 justify-end">
        <Button
          onClick={() => {
            onCancel?.();
            modal.close();
          }}
          look="outlined"
          variant="neutral"
          autoFocus
          aria-label={cancelText ?? "Cancel"}
          data-testid="dialog-cancel-button"
        >
          {cancelText ?? "Cancel"}
        </Button>
 
        <Button
          onClick={() => {
            onOk?.();
            modal.close();
          }}
          variant={buttonLook ?? "primary"}
          aria-label={okText ?? "Confirm"}
          data-testid="dialog-ok-button"
        >
          {okText ?? "OK"}
        </Button>
      </div>
    ),
  });
 
  return modal;
};
 
/**
 * Creates an informational modal with a single OK button.
 */
export const info = <T,>({ okText, onOkPress, ...props }: InfoProps<T>) => {
  const modal = standaloneModal({
    ...props,
    footer: (
      <div className="flex gap-2 justify-end">
        <Button
          onClick={() => {
            onOkPress?.();
            modal.close();
          }}
          aria-label={okText ?? "OK"}
          data-testid="dialog-ok-button"
        >
          {okText ?? "OK"}
        </Button>
      </div>
    ),
  });
 
  return modal;
};
 
export { standaloneModal as modal };
export { Modal };