@sanity/ui 2.7.1-canary.0 → 2.7.1-canary.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/ui",
3
- "version": "2.7.1-canary.0",
3
+ "version": "2.7.1-canary.2",
4
4
  "keywords": [
5
5
  "sanity",
6
6
  "ui",
@@ -116,6 +116,7 @@
116
116
  "@testing-library/dom": "^10.3.2",
117
117
  "@testing-library/jest-dom": "^6.4.6",
118
118
  "@testing-library/react": "^16.0.0",
119
+ "@testing-library/user-event": "^14.5.2",
119
120
  "@types/jest": "^29.5.12",
120
121
  "@types/jest-axe": "^3.5.9",
121
122
  "@types/node": "^20.12.7",
@@ -126,7 +127,7 @@
126
127
  "@typescript-eslint/eslint-plugin": "^7.16.1",
127
128
  "@typescript-eslint/parser": "^7.16.1",
128
129
  "commitizen": "^4.3.0",
129
- "cypress": "^13.13.0",
130
+ "cypress": "^13.13.1",
130
131
  "cypress-real-events": "^1.13.0",
131
132
  "cz-conventional-changelog": "^3.3.0",
132
133
  "eslint": "^8.57.0",
@@ -216,21 +216,18 @@ const DialogCard = forwardRef(function DialogCard(
216
216
  )
217
217
 
218
218
  useClickOutside(
219
- useCallback(
220
- (event: MouseEvent) => {
221
- if (!isTopLayer || !onClickOutside) return
219
+ (event: MouseEvent) => {
220
+ if (!isTopLayer || !onClickOutside) return
222
221
 
223
- const target = event.target as Node | null
222
+ const target = event.target as Node | null
224
223
 
225
- if (target && !isTargetWithinScope(boundaryElement, portalElement, target)) {
226
- // Ignore clicks outside of the scope
227
- return
228
- }
224
+ if (target && !isTargetWithinScope(boundaryElement, portalElement, target)) {
225
+ // Ignore clicks outside of the scope
226
+ return
227
+ }
229
228
 
230
- onClickOutside()
231
- },
232
- [boundaryElement, isTopLayer, onClickOutside, portalElement],
233
- ),
229
+ onClickOutside()
230
+ },
234
231
  [rootElement],
235
232
  )
236
233
 
@@ -93,13 +93,7 @@ export const Menu = forwardRef(function Menu(
93
93
  }, [activeIndex, onItemSelect])
94
94
 
95
95
  // Close menu when clicking outside
96
- useClickOutside(
97
- useCallback(
98
- (event) => isTopLayer && onClickOutside && onClickOutside(event),
99
- [isTopLayer, onClickOutside],
100
- ),
101
- [rootElement],
102
- )
96
+ useClickOutside((event) => isTopLayer && onClickOutside && onClickOutside(event), [rootElement])
103
97
 
104
98
  // Close menu when pressing Escape
105
99
  useGlobalKeyDown(
@@ -0,0 +1,592 @@
1
+ /** @jest-environment jsdom */
2
+ /* eslint-disable padding-line-between-statements, react/display-name */
3
+
4
+ import {render, screen} from '@testing-library/react'
5
+ import userEvent from '@testing-library/user-event'
6
+ import {useEffect, useRef, useState} from 'react'
7
+
8
+ import {useClickOutside} from './useClickOutside'
9
+
10
+ describe('useClickOutside', () => {
11
+ describe('current API', () => {
12
+ /**
13
+ * This suite demonstrates the new API for `useClickOutside` that were introduced in v2.8.0
14
+ */
15
+
16
+ it('calls the handler when clicking outside of the array of elements', async () => {
17
+ const user = userEvent.setup()
18
+ const handler = jest.fn()
19
+
20
+ const TestComponent = () => {
21
+ const buttonRef = useRef<HTMLButtonElement | null>(null)
22
+ const popoverRef = useRef<HTMLDivElement | null>(null)
23
+
24
+ useClickOutside(handler, () => [buttonRef.current, popoverRef.current])
25
+
26
+ return (
27
+ <>
28
+ <button data-testid="button" ref={buttonRef} />
29
+ <div data-testid="popover" ref={popoverRef} />
30
+ <div data-testid="outside" />
31
+ </>
32
+ )
33
+ }
34
+
35
+ render(<TestComponent />)
36
+
37
+ await user.click(screen.getByTestId('button'))
38
+ await user.click(screen.getByTestId('popover'))
39
+ expect(handler).not.toHaveBeenCalled()
40
+
41
+ await user.click(screen.getByTestId('outside'))
42
+ expect(handler).toHaveBeenCalledTimes(1)
43
+ })
44
+
45
+ it('the elements array flattens nested arrays one level deep', async () => {
46
+ const user = userEvent.setup()
47
+ const handler = jest.fn()
48
+
49
+ const TestComponent = () => {
50
+ const buttonRef = useRef<HTMLButtonElement | null>(null)
51
+ const popoverRef = useRef<HTMLDivElement | null>(null)
52
+
53
+ useClickOutside(handler, () => [
54
+ null,
55
+ [null, buttonRef.current],
56
+ [popoverRef.current, null],
57
+ null,
58
+ ])
59
+
60
+ return (
61
+ <>
62
+ <button data-testid="button" ref={buttonRef} />
63
+ <div data-testid="popover" ref={popoverRef} />
64
+ <div data-testid="outside" />
65
+ </>
66
+ )
67
+ }
68
+
69
+ render(<TestComponent />)
70
+
71
+ await user.click(screen.getByTestId('button'))
72
+ await user.click(screen.getByTestId('popover'))
73
+ expect(handler).not.toHaveBeenCalled()
74
+
75
+ await user.click(screen.getByTestId('outside'))
76
+ expect(handler).toHaveBeenCalledTimes(1)
77
+ })
78
+
79
+ it('it can set a boundary to scope outside click events', async () => {
80
+ const user = userEvent.setup()
81
+ const handler = jest.fn()
82
+
83
+ const TestComponent = () => {
84
+ const buttonRef = useRef<HTMLButtonElement | null>(null)
85
+ const popoverRef = useRef<HTMLDivElement | null>(null)
86
+ const boundaryRef = useRef<HTMLDivElement | null>(null)
87
+
88
+ useClickOutside(
89
+ handler,
90
+ () => [buttonRef.current, popoverRef.current],
91
+ () => boundaryRef.current,
92
+ )
93
+
94
+ return (
95
+ <>
96
+ <div ref={boundaryRef}>
97
+ <button data-testid="button" ref={buttonRef} />
98
+ <div data-testid="popover" ref={popoverRef} />
99
+ <div data-testid="inside" />
100
+ </div>
101
+ <div data-testid="outside" />
102
+ </>
103
+ )
104
+ }
105
+
106
+ render(<TestComponent />)
107
+
108
+ await user.click(screen.getByTestId('button'))
109
+ await user.click(screen.getByTestId('popover'))
110
+ // Since it's outside the boundary it should be ignored
111
+ await user.click(screen.getByTestId('outside'))
112
+ expect(handler).not.toHaveBeenCalled()
113
+
114
+ await user.click(screen.getByTestId('inside'))
115
+ expect(handler).toHaveBeenCalledTimes(1)
116
+ })
117
+ })
118
+ })
119
+ describe('legacy API', () => {
120
+ /**
121
+ * Specifying the elements array directly:
122
+ * useClickOutside(handler, [buttonElement, popoverElement])
123
+ * instead of in a callback:
124
+ * useClickOutside(handler, () => [buttonElement, popoverElement])
125
+ * is still supported, but deprecated and discouraged. The same is true for the `setElement` callback pattern
126
+ */
127
+ it('calls the handler when clicking outside of the array of elements', async () => {
128
+ const user = userEvent.setup()
129
+ const handler = jest.fn()
130
+
131
+ const TestComponent = () => {
132
+ const [buttonElement, setButtonElement] = useState<HTMLButtonElement | null>(null)
133
+ const [popoverElement, setPopoverElement] = useState<HTMLDivElement | null>(null)
134
+
135
+ useClickOutside(handler, [buttonElement, popoverElement])
136
+
137
+ return (
138
+ <>
139
+ <button data-testid="button" ref={setButtonElement} />
140
+ <div data-testid="popover" ref={setPopoverElement} />
141
+ <div data-testid="outside" />
142
+ </>
143
+ )
144
+ }
145
+
146
+ render(<TestComponent />)
147
+
148
+ await user.click(screen.getByTestId('button'))
149
+ await user.click(screen.getByTestId('popover'))
150
+ expect(handler).not.toHaveBeenCalled()
151
+
152
+ await user.click(screen.getByTestId('outside'))
153
+ expect(handler).toHaveBeenCalledTimes(1)
154
+ })
155
+
156
+ it('the elements array flattens nested arrays one level deep', async () => {
157
+ const user = userEvent.setup()
158
+ const handler = jest.fn()
159
+
160
+ const TestComponent = () => {
161
+ const [buttonElement, setButtonElement] = useState<HTMLButtonElement | null>(null)
162
+ const [popoverElement, setPopoverElement] = useState<HTMLDivElement | null>(null)
163
+
164
+ useClickOutside(handler, [null, [null, buttonElement], [popoverElement, null], null])
165
+
166
+ return (
167
+ <>
168
+ <button data-testid="button" ref={setButtonElement} />
169
+ <div data-testid="popover" ref={setPopoverElement} />
170
+ <div data-testid="outside" />
171
+ </>
172
+ )
173
+ }
174
+
175
+ render(<TestComponent />)
176
+
177
+ await user.click(screen.getByTestId('button'))
178
+ await user.click(screen.getByTestId('popover'))
179
+ expect(handler).not.toHaveBeenCalled()
180
+
181
+ await user.click(screen.getByTestId('outside'))
182
+ expect(handler).toHaveBeenCalledTimes(1)
183
+ })
184
+
185
+ it('it can set a boundary to scope outside click events', async () => {
186
+ const user = userEvent.setup()
187
+ const handler = jest.fn()
188
+
189
+ const TestComponent = () => {
190
+ const [buttonElement, setButtonElement] = useState<HTMLButtonElement | null>(null)
191
+ const [popoverElement, setPopoverElement] = useState<HTMLDivElement | null>(null)
192
+ const [boundaryElement, setBoundaryElement] = useState<HTMLDivElement | null>(null)
193
+
194
+ useClickOutside(handler, [buttonElement, popoverElement], boundaryElement)
195
+
196
+ return (
197
+ <>
198
+ <div ref={setBoundaryElement}>
199
+ <button data-testid="button" ref={setButtonElement} />
200
+ <div data-testid="popover" ref={setPopoverElement} />
201
+ <div data-testid="inside" />
202
+ </div>
203
+ <div data-testid="outside" />
204
+ </>
205
+ )
206
+ }
207
+
208
+ render(<TestComponent />)
209
+
210
+ await user.click(screen.getByTestId('button'))
211
+ await user.click(screen.getByTestId('popover'))
212
+ // Since it's outside the boundary it should be ignored
213
+ await user.click(screen.getByTestId('outside'))
214
+ expect(handler).not.toHaveBeenCalled()
215
+
216
+ await user.click(screen.getByTestId('inside'))
217
+ expect(handler).toHaveBeenCalledTimes(1)
218
+ })
219
+
220
+ it('it returns a `setElement` callback', async () => {
221
+ /**
222
+ * We don't use this pattern in the studio codebase anymore, and we don't recommend using it moving forward.
223
+ * But since it's part of the public API we can't remove it without a major version bump.
224
+ * Thus it makes sense to unit test it to ensure it works as expected and we don't accidentally break backwards compatibility.
225
+ */
226
+ const user = userEvent.setup()
227
+ const handler = jest.fn()
228
+
229
+ const TestComponent = (props: {open?: true}) => {
230
+ const {open = false} = props
231
+ const [buttonElement, setButtonElement] = useState<HTMLButtonElement | null>(null)
232
+
233
+ const setElement = useClickOutside(handler, [buttonElement])
234
+
235
+ return (
236
+ <>
237
+ <button data-testid="button" ref={setButtonElement} />
238
+ {open && <div data-testid="popover" ref={setElement} />}
239
+ <div data-testid="outside" />
240
+ </>
241
+ )
242
+ }
243
+
244
+ const {rerender} = render(<TestComponent />)
245
+
246
+ await user.click(screen.getByTestId('button'))
247
+ expect(handler).not.toHaveBeenCalled()
248
+
249
+ await user.click(screen.getByTestId('outside'))
250
+ expect(handler).toHaveBeenCalledTimes(1)
251
+
252
+ // The popover isn't rendered yet
253
+ expect(screen.queryByTestId('popover')).toBeNull()
254
+
255
+ // Rerender the component with the popover open
256
+ rerender(<TestComponent open />)
257
+
258
+ // Clicking the popover should not trigger the handler
259
+ await user.click(screen.getByTestId('popover'))
260
+ expect(handler).toHaveBeenCalledTimes(1)
261
+ })
262
+
263
+ it('instead of `setElement`, update the `elements` array', async () => {
264
+ /**
265
+ * Since we don't want people to use the `setElement` pattern, test that userland can handle dynamically changing elements arrays
266
+ */
267
+ const user = userEvent.setup()
268
+ const handler = jest.fn()
269
+
270
+ const TestComponent = (props: {open?: true}) => {
271
+ const {open = false} = props
272
+ const [buttonElement, setButtonElement] = useState<HTMLButtonElement | null>(null)
273
+ const [popoverElement, setPopoverElement] = useState<HTMLDivElement | null>(null)
274
+
275
+ useClickOutside(handler, [buttonElement, popoverElement])
276
+
277
+ return (
278
+ <>
279
+ <button data-testid="button" ref={setButtonElement} />
280
+ {open && <div data-testid="popover" ref={setPopoverElement} />}
281
+ <div data-testid="outside" />
282
+ </>
283
+ )
284
+ }
285
+
286
+ const {rerender} = render(<TestComponent />)
287
+
288
+ await user.click(screen.getByTestId('button'))
289
+ expect(handler).not.toHaveBeenCalled()
290
+
291
+ await user.click(screen.getByTestId('outside'))
292
+ expect(handler).toHaveBeenCalledTimes(1)
293
+
294
+ // The popover isn't rendered yet
295
+ expect(screen.queryByTestId('popover')).toBeNull()
296
+
297
+ // Rerender the component with the popover open
298
+ rerender(<TestComponent open />)
299
+
300
+ // Clicking the popover should not trigger the handler
301
+ await user.click(screen.getByTestId('popover'))
302
+ expect(handler).toHaveBeenCalledTimes(1)
303
+ })
304
+
305
+ it('returning the current value of refs in the elements array is dangerous', async () => {
306
+ /**
307
+ * This test demonstrates why it's dangerous to return ref values in the elements array.
308
+ * When using refs, pass a function returning the elements array, instead of defining the array directly during render
309
+ */
310
+
311
+ const user = userEvent.setup()
312
+ let handler = jest.fn()
313
+
314
+ /**
315
+ * Using refs in the `useClickOutside` elements array is dangerous,
316
+ * the below example demonstrates how `useClickOutside` doesn't "see" the current values of refs,
317
+ * it can only "see" whatever the value of the ref was when the hook was rendered.
318
+ */
319
+ let TestComponent = () => {
320
+ const buttonRef = useRef<HTMLButtonElement | null>(null)
321
+ const popoverRef = useRef<HTMLDivElement | null>(null)
322
+
323
+ useClickOutside(handler, [buttonRef.current, popoverRef.current])
324
+
325
+ return (
326
+ <>
327
+ <button data-testid="button" ref={buttonRef} />
328
+ <div data-testid="popover" ref={popoverRef} />
329
+ </>
330
+ )
331
+ }
332
+ const {rerender} = render(<TestComponent />)
333
+ await user.click(screen.getByTestId('button'))
334
+ await user.click(screen.getByTestId('popover'))
335
+ // Because the ref values are stale, the handler is called
336
+ expect(handler).toHaveBeenCalledTimes(2)
337
+
338
+ /**
339
+ * If a mixture of refs and state is used it can appear like it's working correctly,
340
+ * but this is a side-effect, not an indication it's safe.
341
+ */
342
+ handler = jest.fn()
343
+ TestComponent = () => {
344
+ const buttonRef = useRef<HTMLButtonElement | null>(null)
345
+ const [popoverElement, setPopoverElement] = useState<HTMLDivElement | null>(null)
346
+
347
+ useClickOutside(handler, [buttonRef.current, popoverElement])
348
+
349
+ return (
350
+ <>
351
+ <button data-testid="button" ref={buttonRef} />
352
+ <div data-testid="popover" ref={setPopoverElement} />
353
+ <div data-testid="outside" />
354
+ </>
355
+ )
356
+ }
357
+ rerender(<TestComponent />)
358
+ await user.click(screen.getByTestId('button'))
359
+ await user.click(screen.getByTestId('popover'))
360
+ expect(handler).not.toHaveBeenCalled()
361
+ await user.click(screen.getByTestId('outside'))
362
+ expect(handler).toHaveBeenCalledTimes(1)
363
+
364
+ /**
365
+ * Unrelated state updates can create the same false impression of safety.
366
+ */
367
+ handler = jest.fn()
368
+ TestComponent = () => {
369
+ const buttonRef = useRef<HTMLButtonElement | null>(null)
370
+ const popoverRef = useRef<HTMLDivElement | null>(null)
371
+
372
+ useClickOutside(handler, [buttonRef.current, popoverRef.current])
373
+
374
+ const [, tick] = useState(0)
375
+ useEffect(() => {
376
+ /**
377
+ * This effect schedules a re-render, which will lead to `useClickOutsideHandler` "seeing"
378
+ * the current value of the refs after they got assigned dom nodes when the React ref callbacks executed.
379
+ */
380
+ tick((prev) => ++prev)
381
+ }, [])
382
+
383
+ return (
384
+ <>
385
+ <button data-testid="button" ref={buttonRef} />
386
+ <div data-testid="popover" ref={popoverRef} />
387
+ <div data-testid="outside" />
388
+ </>
389
+ )
390
+ }
391
+ rerender(<TestComponent />)
392
+ await user.click(screen.getByTestId('button'))
393
+ await user.click(screen.getByTestId('popover'))
394
+ expect(handler).not.toHaveBeenCalled()
395
+ await user.click(screen.getByTestId('outside'))
396
+ expect(handler).toHaveBeenCalledTimes(1)
397
+
398
+ /**
399
+ * When using the legacy version of the `useClickOutside` API it's necessary to synchronize mutable ref values
400
+ * with a effect and state loop to ensure they're not stale
401
+ */
402
+ const useElementsFromRefs = (refs: React.MutableRefObject<HTMLElement | null>[]) => {
403
+ const [elements, setElements] = useState(() => refs.map((ref) => ref.current))
404
+
405
+ useEffect(() => {
406
+ if (refs.length !== elements.length) {
407
+ setElements(refs.map((ref) => ref.current))
408
+ }
409
+ for (const ref of refs) {
410
+ if (!elements.includes(ref.current)) {
411
+ setElements(refs.map((ref) => ref.current))
412
+ return
413
+ }
414
+ }
415
+ }, [elements, refs])
416
+
417
+ return elements
418
+ }
419
+ handler = jest.fn()
420
+ TestComponent = () => {
421
+ const buttonRef = useRef<HTMLButtonElement | null>(null)
422
+ const popoverRef = useRef<HTMLDivElement | null>(null)
423
+
424
+ const elements = useElementsFromRefs([buttonRef, popoverRef])
425
+ useClickOutside(handler, elements)
426
+
427
+ return (
428
+ <>
429
+ <button data-testid="button" ref={buttonRef} />
430
+ <div data-testid="popover" ref={popoverRef} />
431
+ <div data-testid="outside" />
432
+ </>
433
+ )
434
+ }
435
+ rerender(<TestComponent />)
436
+ await user.click(screen.getByTestId('button'))
437
+ await user.click(screen.getByTestId('popover'))
438
+ expect(handler).not.toHaveBeenCalled()
439
+ await user.click(screen.getByTestId('outside'))
440
+ expect(handler).toHaveBeenCalledTimes(1)
441
+ })
442
+
443
+ it('using the current value of a react ref as the `boundaryElement` is dangerous', async () => {
444
+ /**
445
+ * This test demonstrates why it's dangerous to return a ref value as the boundaryElement,
446
+ * for the same reasons it's dangerous for the elements array.
447
+ */
448
+
449
+ const user = userEvent.setup()
450
+ let handler = jest.fn()
451
+
452
+ /**
453
+ * Using refs in the `useClickOutside` elements array is dangerous,
454
+ * the below example demonstrates how `useClickOutside` doesn't "see" the current values of refs,
455
+ * it can only "see" whatever the value of the ref was when the hook was rendered.
456
+ */
457
+ let TestComponent = () => {
458
+ const boundaryRef = useRef<HTMLDivElement | null>(null)
459
+
460
+ useClickOutside(handler, [], boundaryRef.current)
461
+
462
+ return (
463
+ <>
464
+ <div ref={boundaryRef}>
465
+ <div data-testid="inside" />
466
+ </div>
467
+ <div data-testid="outside" />
468
+ </>
469
+ )
470
+ }
471
+ const {rerender} = render(<TestComponent />)
472
+ await user.click(screen.getByTestId('inside'))
473
+ await user.click(screen.getByTestId('outside'))
474
+ // Because the ref values are stale, the handler is called
475
+ expect(handler).toHaveBeenCalledTimes(2)
476
+
477
+ /**
478
+ * If a mixture of refs and state is used it can appear like it's working correctly,
479
+ * but this is a side-effect, not an indication it's safe.
480
+ */
481
+ handler = jest.fn()
482
+ TestComponent = () => {
483
+ const buttonRef = useRef<HTMLButtonElement | null>(null)
484
+ const [popoverElement, setPopoverElement] = useState<HTMLDivElement | null>(null)
485
+ const boundaryRef = useRef<HTMLDivElement | null>(null)
486
+
487
+ useClickOutside(handler, [buttonRef.current, popoverElement], boundaryRef.current)
488
+
489
+ return (
490
+ <>
491
+ <div ref={boundaryRef}>
492
+ <button data-testid="button" ref={buttonRef} />
493
+ <div data-testid="popover" ref={setPopoverElement} />
494
+ <div data-testid="inside" />
495
+ </div>
496
+ <div data-testid="outside" />
497
+ </>
498
+ )
499
+ }
500
+ rerender(<TestComponent />)
501
+ await user.click(screen.getByTestId('button'))
502
+ await user.click(screen.getByTestId('popover'))
503
+ await user.click(screen.getByTestId('outside'))
504
+ expect(handler).not.toHaveBeenCalled()
505
+ await user.click(screen.getByTestId('inside'))
506
+ expect(handler).toHaveBeenCalledTimes(1)
507
+
508
+ /**
509
+ * Unrelated state updates can create the same false impression of safety.
510
+ */
511
+ handler = jest.fn()
512
+ TestComponent = () => {
513
+ const buttonRef = useRef<HTMLButtonElement | null>(null)
514
+ const popoverRef = useRef<HTMLDivElement | null>(null)
515
+ const boundaryRef = useRef<HTMLDivElement | null>(null)
516
+
517
+ useClickOutside(handler, [buttonRef.current, popoverRef.current], boundaryRef.current)
518
+
519
+ const [, tick] = useState(0)
520
+ useEffect(() => {
521
+ /**
522
+ * This effect schedules a re-render, which will lead to `useClickOutsideHandler` "seeing"
523
+ * the current value of the refs after they got assigned dom nodes when the React ref callbacks executed.
524
+ */
525
+ tick((prev) => ++prev)
526
+ }, [])
527
+
528
+ return (
529
+ <>
530
+ <div ref={boundaryRef}>
531
+ <button data-testid="button" ref={buttonRef} />
532
+ <div data-testid="popover" ref={popoverRef} />
533
+ <div data-testid="inside" />
534
+ </div>
535
+ <div data-testid="outside" />
536
+ </>
537
+ )
538
+ }
539
+ rerender(<TestComponent />)
540
+ await user.click(screen.getByTestId('button'))
541
+ await user.click(screen.getByTestId('popover'))
542
+ await user.click(screen.getByTestId('outside'))
543
+ expect(handler).not.toHaveBeenCalled()
544
+ await user.click(screen.getByTestId('inside'))
545
+ expect(handler).toHaveBeenCalledTimes(1)
546
+
547
+ /**
548
+ * When using the legacy version of the `useClickOutside` API it's necessary to synchronize mutable ref values
549
+ * with a effect and state loop to ensure they're not stale
550
+ */
551
+ const useBoundaryElementFromRef = (ref: React.MutableRefObject<HTMLElement | null>) => {
552
+ const [element, setElement] = useState(() => ref.current)
553
+
554
+ // eslint-disable-next-line react-hooks/exhaustive-deps
555
+ useEffect(() => {
556
+ // If the ref has mutated since the last render, update the state and schedule a re-render
557
+ if (ref.current !== element) {
558
+ setElement(ref.current)
559
+ }
560
+ })
561
+
562
+ return element
563
+ }
564
+ handler = jest.fn()
565
+ TestComponent = () => {
566
+ const buttonRef = useRef<HTMLButtonElement | null>(null)
567
+ const popoverRef = useRef<HTMLDivElement | null>(null)
568
+ const boundaryRef = useRef<HTMLDivElement | null>(null)
569
+
570
+ const boundaryElement = useBoundaryElementFromRef(boundaryRef)
571
+ useClickOutside(handler, [buttonRef.current, popoverRef.current], boundaryElement)
572
+
573
+ return (
574
+ <>
575
+ <div ref={boundaryRef}>
576
+ <button data-testid="button" ref={buttonRef} />
577
+ <div data-testid="popover" ref={popoverRef} />
578
+ <div data-testid="inside" />
579
+ </div>
580
+ <div data-testid="outside" />
581
+ </>
582
+ )
583
+ }
584
+ rerender(<TestComponent />)
585
+ await user.click(screen.getByTestId('button'))
586
+ await user.click(screen.getByTestId('popover'))
587
+ await user.click(screen.getByTestId('outside'))
588
+ expect(handler).not.toHaveBeenCalled()
589
+ await user.click(screen.getByTestId('inside'))
590
+ expect(handler).toHaveBeenCalledTimes(1)
591
+ })
592
+ })