• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CBIIT / crdc-datahub-ui / 21080480968

16 Jan 2026 08:51PM UTC coverage: 80.151% (+0.8%) from 79.35%
21080480968

Pull #932

github

web-flow
Merge 2838b9429 into 9dfdabde2
Pull Request #932: CRDCDH-3398 Created ChatBot prototype

5536 of 6059 branches covered (91.37%)

Branch coverage included in aggregate %.

1710 of 1753 new or added lines in 19 files covered. (97.55%)

1 existing line in 1 file now uncovered.

32566 of 41479 relevant lines covered (78.51%)

223.58 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.46
/src/components/ChatBot/hooks/useChatDrawer.ts
1
import React, { useCallback, useEffect, useReducer, useRef } from "react";
1✔
2

3
import chatConfig from "../config/chatConfig";
1✔
4
import { getViewportHeightPx } from "../utils/chatUtils";
1✔
5

6
type DrawerState = {
7
  /**
8
   * Indicates whether the drawer is currently open.
9
   */
10
  isOpen: boolean;
11
  /**
12
   * Indicates whether the drawer is currently being dragged/resized.
13
   */
14
  isDragging: boolean;
15
  /**
16
   * Indicates whether the drawer is currently expanded to its maximum height.
17
   */
18
  isExpanded: boolean;
19
  /**
20
   * The current height of the drawer in pixels.
21
   */
22
  heightPx: number;
23
  /**
24
   * The current width of the drawer in pixels.
25
   */
26
  widthPx: number;
27
};
28

29
type DrawerAction =
30
  | { type: "opened" }
31
  | { type: "closed" }
32
  | { type: "drag_started" }
33
  | { type: "drag_ended" }
34
  | { type: "height_changed"; heightPx: number; viewportHeightPx: number }
35
  | { type: "width_changed"; widthPx: number }
36
  | { type: "expand_toggled"; viewportHeightPx: number };
37

38
/**
39
 * Reducer function to manage the state of the chat drawer.
40
 *
41
 * @param {DrawerState} state - The current state of the chat drawer.
42
 * @param {DrawerAction} action - The action to be performed on the chat drawer state.
43
 * @returns The new state of the chat drawer after applying the action.
44
 */
45
const reducer = (state: DrawerState, action: DrawerAction): DrawerState => {
1✔
46
  switch (action.type) {
84✔
47
    case "opened": {
84✔
48
      if (state.isOpen) {
32✔
49
        return state;
1✔
50
      }
1✔
51

52
      return {
31✔
53
        isOpen: true,
31✔
54
        isDragging: false,
31✔
55
        isExpanded: false,
31✔
56
        heightPx: chatConfig.height.collapsed,
31✔
57
        widthPx: chatConfig.width.default,
31✔
58
      };
31✔
59
    }
31✔
60
    case "closed": {
84✔
61
      if (!state.isOpen) {
7✔
62
        return state;
1✔
63
      }
1✔
64

65
      return {
6✔
66
        ...state,
6✔
67
        isOpen: false,
6✔
68
        isDragging: false,
6✔
69
        isExpanded: false,
6✔
70
        heightPx: chatConfig.height.collapsed,
6✔
71
        widthPx: chatConfig.width.default,
6✔
72
      };
6✔
73
    }
6✔
74
    case "drag_started": {
84✔
75
      if (!state.isOpen || state.isDragging) {
25✔
76
        return state;
10✔
77
      }
10✔
78

79
      return { ...state, isDragging: true };
15✔
80
    }
15✔
81
    case "drag_ended": {
84✔
82
      if (!state.isDragging) {
3!
NEW
83
        return state;
×
NEW
84
      }
×
85

86
      return { ...state, isDragging: false };
3✔
87
    }
3✔
88
    case "height_changed": {
84✔
89
      const isNearMax =
4✔
90
        action.viewportHeightPx - action.heightPx <= chatConfig.height.expandedSnapThreshold;
4✔
91
      return {
4✔
92
        ...state,
4✔
93
        heightPx: action.heightPx,
4✔
94
        isExpanded: isNearMax,
4✔
95
      };
4✔
96
    }
4✔
97
    case "width_changed": {
84✔
98
      return {
4✔
99
        ...state,
4✔
100
        widthPx: action.widthPx,
4✔
101
      };
4✔
102
    }
4✔
103
    case "expand_toggled": {
84✔
104
      if (!state.isOpen) {
9✔
105
        return state;
1✔
106
      }
1✔
107

108
      if (state.isExpanded) {
9✔
109
        return {
2✔
110
          ...state,
2✔
111
          isExpanded: false,
2✔
112
          heightPx: chatConfig.height.collapsed,
2✔
113
        };
2✔
114
      }
2✔
115

116
      return {
6✔
117
        ...state,
6✔
118
        isExpanded: true,
6✔
119
        heightPx: action.viewportHeightPx,
6✔
120
      };
6✔
121
    }
6✔
122
    default: {
84!
NEW
123
      return state;
×
NEW
124
    }
×
125
  }
84✔
126
};
84✔
127

128
export type useChatDrawerResult = {
129
  drawerRef: React.RefObject<HTMLDivElement>;
130
  isOpen: boolean;
131
  isDragging: boolean;
132
  isExpanded: boolean;
133
  drawerHeightPx: number;
134
  drawerWidthPx: number;
135
  openDrawer: () => void;
136
  closeDrawer: () => void;
137
  beginResize: React.PointerEventHandler<HTMLDivElement>;
138
  toggleExpand: () => void;
139
};
140

141
/**
142
 * Custom hook to manage the state and behavior of the chat drawer.
143
 *
144
 * @returns An object containing the state and actions for the chat drawer.
145
 */
146
export const useChatDrawer = (): useChatDrawerResult => {
1✔
147
  const drawerRef = useRef<HTMLDivElement>(null);
120✔
148

149
  const [state, dispatch] = useReducer(reducer, {
120✔
150
    isOpen: false,
120✔
151
    isDragging: false,
120✔
152
    isExpanded: false,
120✔
153
    heightPx: chatConfig.height.collapsed,
120✔
154
    widthPx: chatConfig.width.default,
120✔
155
  });
120✔
156

157
  const stateRef = useRef(state);
120✔
158
  stateRef.current = state;
120✔
159

160
  /**
161
   * These refs ensure the global event handlers have immediate, up-to-date state
162
   * without needing to re-register listeners.
163
   */
164
  const isDraggingRef = useRef(false);
120✔
165
  /**
166
   * Stores the ID of the active pointer during a drag operation.
167
   */
168
  const activePointerIdRef = useRef<number | null>(null);
120✔
169
  /**
170
   * Stores the initial pointer position when drag starts.
171
   */
172
  const initialPointerPosRef = useRef<{ x: number; y: number } | null>(null);
120✔
173
  /**
174
   * Stores the initial dimensions when drag starts.
175
   */
176
  const initialDimensionsRef = useRef<{ width: number; height: number } | null>(null);
120✔
177

178
  /**
179
   * Opens the chat drawer.
180
   */
181
  const openDrawer = useCallback((): void => {
120✔
182
    dispatch({ type: "opened" });
32✔
183
  }, []);
120✔
184

185
  /**
186
   * Closes the chat drawer.
187
   */
188
  const closeDrawer = useCallback((): void => {
120✔
189
    isDraggingRef.current = false;
7✔
190
    activePointerIdRef.current = null;
7✔
191
    dispatch({ type: "closed" });
7✔
192
  }, []);
120✔
193

194
  /**
195
   * Toggles the expanded state of the chat drawer.
196
   */
197
  const toggleExpand = useCallback((): void => {
120✔
198
    const viewportHeightPx = getViewportHeightPx(chatConfig.height.collapsed);
9✔
199
    dispatch({ type: "expand_toggled", viewportHeightPx });
9✔
200
  }, []);
120✔
201

202
  /**
203
   * Begins the resize operation for the chat drawer.
204
   */
205
  const beginResize: React.PointerEventHandler<HTMLDivElement> = useCallback((event): void => {
120✔
206
    if (event.pointerType === "mouse" && event.button !== 0) {
27✔
207
      return;
1✔
208
    }
1✔
209

210
    if (!stateRef.current.isOpen) {
27✔
211
      return;
1✔
212
    }
1✔
213

214
    event.preventDefault();
25✔
215

216
    isDraggingRef.current = true;
25✔
217
    activePointerIdRef.current = event.pointerId;
25✔
218
    initialPointerPosRef.current = { x: event.clientX, y: event.clientY };
25✔
219
    initialDimensionsRef.current = {
25✔
220
      width: stateRef.current.widthPx,
25✔
221
      height: stateRef.current.heightPx,
25✔
222
    };
25✔
223

224
    dispatch({ type: "drag_started" });
25✔
225
  }, []);
120✔
226

227
  /**
228
   * Applies the resize, given pointer X and Y positions.
229
   */
230
  const applyResize = useCallback((clientX: number, clientY: number): void => {
120✔
231
    const drawerElement = drawerRef.current;
5✔
232
    if (!drawerElement) {
5✔
233
      return;
1✔
234
    }
1✔
235

236
    const initialPos = initialPointerPosRef.current;
4✔
237
    const initialDims = initialDimensionsRef.current;
4✔
238

239
    if (!initialPos || !initialDims) {
5!
NEW
240
      return;
×
NEW
241
    }
✔
242

243
    // Calculate deltas from initial position
244
    const deltaX = initialPos.x - clientX;
4✔
245
    const deltaY = initialPos.y - clientY;
4✔
246

247
    // Apply delta to initial dimensions
248
    const newWidth = initialDims.width + deltaX;
4✔
249
    const newHeight = initialDims.height + deltaY;
4✔
250

251
    // Update height
252
    const viewportHeightPx = getViewportHeightPx(chatConfig.height.collapsed);
4✔
253
    const heightPx = Math.max(chatConfig.height.min, Math.min(newHeight, viewportHeightPx));
4✔
254
    dispatch({
4✔
255
      type: "height_changed",
4✔
256
      heightPx,
4✔
257
      viewportHeightPx,
4✔
258
    });
4✔
259

260
    // Update width
261
    const viewportWidth = window.innerWidth;
4✔
262
    const widthPx = Math.max(chatConfig.width.min, Math.min(newWidth, viewportWidth));
4✔
263
    dispatch({
4✔
264
      type: "width_changed",
4✔
265
      widthPx,
4✔
266
    });
4✔
267
  }, []);
120✔
268

269
  /**
270
   * Handles the pointer move event on the window.
271
   */
272
  const handleWindowPointerMove = useCallback(
120✔
273
    (event: PointerEvent): void => {
120✔
274
      if (!isDraggingRef.current) {
55✔
275
        return;
48✔
276
      }
48✔
277

278
      const activePointerId = activePointerIdRef.current;
7✔
279
      if (activePointerId !== null && event.pointerId !== activePointerId) {
55✔
280
        return;
2✔
281
      }
2✔
282

283
      applyResize(event.clientX, event.clientY);
5✔
284
    },
55✔
285
    [applyResize]
120✔
286
  );
120✔
287

288
  /**
289
   * Ends the drag operation for the chat drawer.
290
   */
291
  const endDrag = useCallback((): void => {
120✔
292
    if (!isDraggingRef.current) {
52✔
293
      return;
49✔
294
    }
49✔
295

296
    isDraggingRef.current = false;
3✔
297
    activePointerIdRef.current = null;
3✔
298
    initialPointerPosRef.current = null;
3✔
299
    initialDimensionsRef.current = null;
3✔
300

301
    dispatch({ type: "drag_ended" });
3✔
302
  }, []);
120✔
303

304
  /**
305
   * Handles the pointer up event on the window.
306
   */
307
  const handleWindowPointerUp = useCallback(
120✔
308
    (event: PointerEvent): void => {
120✔
309
      const activePointerId = activePointerIdRef.current;
54✔
310

311
      if (activePointerId === null || event.pointerId === activePointerId) {
54✔
312
        endDrag();
52✔
313
      }
52✔
314
    },
54✔
315
    [endDrag]
120✔
316
  );
120✔
317

318
  useEffect(() => {
120✔
319
    window.addEventListener("pointermove", handleWindowPointerMove);
39✔
320
    window.addEventListener("pointerup", handleWindowPointerUp);
39✔
321
    window.addEventListener("pointercancel", handleWindowPointerUp);
39✔
322

323
    return () => {
39✔
324
      window.removeEventListener("pointermove", handleWindowPointerMove);
39✔
325
      window.removeEventListener("pointerup", handleWindowPointerUp);
39✔
326
      window.removeEventListener("pointercancel", handleWindowPointerUp);
39✔
327
    };
39✔
328
  }, [handleWindowPointerMove, handleWindowPointerUp]);
120✔
329

330
  return {
120✔
331
    drawerRef,
120✔
332
    isOpen: state.isOpen,
120✔
333
    isDragging: state.isDragging,
120✔
334
    isExpanded: state.isExpanded,
120✔
335
    drawerHeightPx: state.heightPx,
120✔
336
    drawerWidthPx: state.widthPx,
120✔
337
    openDrawer,
120✔
338
    closeDrawer,
120✔
339
    beginResize,
120✔
340
    toggleExpand,
120✔
341
  };
120✔
342
};
120✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc