Bin
2025-12-16 9e0b2ba2c317b1a86212f24cbae3195ad1f3dbfa
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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
import {
  Children,
  cloneElement,
  forwardRef,
  type RefObject,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import { Dropdown, type DropdownProps, type DropdownRef } from "./dropdown";
import { DropdownContext, type DropdownContextValue } from "./dropdown-context";
 
// Simple incrementing counter for z-index stacking
// Dropdowns are portaled to document.body, so parent z-index doesn't matter
let zIndexCounter = 0;
 
interface DropdownTriggerProps extends DropdownProps {
  /** HTML tag to use for the trigger wrapper */
  tag?: string;
  /** External dropdown ref */
  dropdown?: RefObject<DropdownRef>;
  /** Content to render in the dropdown */
  content?: React.ReactNode;
  /** Data-testid attribute for testing */
  dataTestId?: string;
  /** If false, clicking trigger only opens dropdown (doesn't toggle) */
  toggle?: boolean;
  /** Close dropdown when clicking outside */
  closeOnClickOutside?: boolean;
  /** Disable the dropdown trigger */
  disabled?: boolean;
  /** CSS class name for the trigger */
  className?: string;
  /** Custom validation function to check if an element should be considered part of the dropdown (from DataManager) */
  isChildValid?: (element: HTMLElement) => boolean;
  /** Children elements (trigger element) */
  children?: React.ReactNode;
}
 
export const DropdownTrigger = forwardRef<DropdownRef, DropdownTriggerProps>(
  (
    {
      tag,
      children,
      content,
      toggle,
      closeOnClickOutside = true,
      disabled = false,
      isChildValid = () => false,
      dropdown,
      ...props
    },
    ref,
  ) => {
    const dropdownRef = (dropdown ?? ref ?? useRef<DropdownRef>()) as RefObject<DropdownRef>;
    const triggerEL = Children.only(children);
    const childset = useRef(new Set<DropdownContextValue>());
    const [isOpen, setIsOpen] = useState(false);
 
    // Assign a unique z-index for this dropdown
    const minIndex = useMemo(() => 1000 + zIndexCounter++, []);
 
    const triggerRef = useRef<HTMLElement>((triggerEL as any)?.props?.ref?.current);
    const parentDropdown = useContext(DropdownContext);
 
    const targetIsInsideDropdown = useCallback(
      (target: HTMLElement) => {
        const triggerClicked = triggerRef.current?.contains?.(target);
        if (triggerClicked) return true;
 
        const dropdownClicked = dropdownRef.current?.dropdown?.contains?.(target);
        if (dropdownClicked) return true;
 
        if (isChildValid(target)) return true;
 
        // Check child dropdowns - short-circuit on first match
        for (const child of childset.current) {
          if (child.hasTarget(target)) return true;
        }
 
        return false;
      },
      [triggerRef, dropdownRef, isChildValid],
    );
 
    const handleClick = useCallback(
      (e: any) => {
        // If dropdown is not visible, bail out immediately - this is critical for performance
        // when there are many nested dropdowns (e.g., 84+ in notifications list)
        if (!dropdownRef.current?.visible) return;
        if (!closeOnClickOutside) return;
 
        // Fast path: check our own trigger and dropdown first before expensive child iteration
        const target = e.target;
        if (triggerRef.current?.contains?.(target)) return;
        if (dropdownRef.current?.dropdown?.contains?.(target)) return;
        if (isChildValid(target)) return;
 
        // Only check children if we didn't match trigger/dropdown
        if (targetIsInsideDropdown(target)) return;
 
        dropdownRef.current?.close?.();
      },
      [closeOnClickOutside, targetIsInsideDropdown, triggerRef, dropdownRef, isChildValid],
    );
 
    const handleToggle = useCallback(
      (e: any) => {
        if (disabled) return;
 
        const inDropdown = dropdownRef.current?.dropdown?.contains?.(e.target);
 
        if (inDropdown) return e.stopPropagation();
 
        if (toggle === false) {
          return dropdownRef?.current?.open();
        }
 
        dropdownRef?.current?.toggle();
      },
      [dropdownRef, disabled, toggle],
    );
 
    const cloneProps = useMemo(() => {
      return {
        ...(triggerEL as any).props,
        tag,
        key: "dd-trigger",
        ref: (el: HTMLElement) => {
          triggerRef.current = triggerRef.current ?? el;
        },
        onClickCapture: handleToggle,
      };
    }, [triggerEL, triggerRef, handleToggle, tag]);
 
    const triggerClone = useMemo(() => {
      return cloneElement(triggerEL as any, cloneProps);
    }, [triggerEL, cloneProps]);
 
    const dropdownClone = content ? (
      <Dropdown
        {...props}
        ref={dropdownRef}
        onToggle={(visible) => {
          setIsOpen(visible);
          props.onToggle?.(visible);
        }}
      >
        {content}
      </Dropdown>
    ) : null;
 
    useEffect(() => {
      // For external dropdowns (no content), always add listener since we can't track visibility via onToggle
      // For internal dropdowns (with content), only add when open for performance
      const shouldAddListener = content ? isOpen : true;
 
      if (!shouldAddListener) return;
 
      document.addEventListener("click", handleClick, { capture: true });
 
      return () => {
        document.removeEventListener("click", handleClick, { capture: true });
      };
    }, [handleClick, isOpen, content]);
 
    const contextValue = useMemo((): DropdownContextValue => {
      return {
        minIndex,
        triggerRef,
        dropdown: dropdownRef,
        hasTarget: (target: HTMLElement) => {
          // Inline the function to avoid dependency issues
          const triggerClicked = triggerRef.current?.contains?.(target);
          if (triggerClicked) return true;
 
          const dropdownClicked = dropdownRef.current?.dropdown?.contains?.(target);
          if (dropdownClicked) return true;
 
          if (isChildValid(target)) return true;
 
          for (const child of childset.current) {
            if (child.hasTarget(target)) return true;
          }
 
          return false;
        },
        addChild: (child) => childset.current.add(child),
        removeChild: (child) => childset.current.delete(child),
        open: () => dropdownRef?.current?.open?.(),
        close: () => dropdownRef?.current?.close?.(),
      };
    }, [triggerRef, dropdownRef, minIndex, isChildValid]);
 
    useEffect(() => {
      if (!parentDropdown) return;
 
      parentDropdown.addChild(contextValue);
      return () => {
        parentDropdown.removeChild(contextValue);
      };
    }, []); // Empty deps - only register once on mount, unregister on unmount
 
    return (
      <DropdownContext.Provider value={contextValue}>
        {triggerClone}
        {dropdownClone}
      </DropdownContext.Provider>
    );
  },
);
 
DropdownTrigger.displayName = "DropdownTrigger";
 
export const useDropdown = () => {
  return useContext(DropdownContext);
};