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

JedWatson / react-select / 709cb899-8707-41eb-8849-9acb620d1412

12 Aug 2024 12:33PM UTC coverage: 75.674% (-0.1%) from 75.789%
709cb899-8707-41eb-8849-9acb620d1412

Pull #6056

circleci

konstantinbabur
Nested groups support
Pull Request #6056: Nested groups support

660 of 1056 branches covered (62.5%)

18 of 23 new or added lines in 1 file covered. (78.26%)

116 existing lines in 2 files now uncovered.

1039 of 1373 relevant lines covered (75.67%)

1923.58 hits per line

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

34.38
/packages/react-select/src/components/Menu.tsx
1
/** @jsx jsx */
2
import {
3
  createContext,
4
  ReactElement,
5
  ReactNode,
6
  Ref,
7
  useCallback,
8
  useContext,
9
  useMemo,
10
  useRef,
11
  useState,
12
} from 'react';
13
import { jsx } from '@emotion/react';
14
import { createPortal } from 'react-dom';
15
import { autoUpdate } from '@floating-ui/dom';
16
import useLayoutEffect from 'use-isomorphic-layout-effect';
17

18
import {
19
  animatedScrollTo,
20
  getBoundingClientObj,
21
  getScrollParent,
22
  getScrollTop,
23
  getStyleProps,
24
  normalizedHeight,
25
  scrollTo,
26
} from '../utils';
27
import {
28
  MenuPlacement,
29
  MenuPosition,
30
  CommonProps,
31
  GroupBase,
32
  CommonPropsAndClassName,
33
  CoercedMenuPlacement,
34
  CSSObjectWithLabel,
35
} from '../types';
36

37
// ==============================
38
// Menu
39
// ==============================
40

41
// Get Menu Placement
42
// ------------------------------
43

44
interface CalculatedMenuPlacementAndHeight {
45
  placement: CoercedMenuPlacement;
46
  maxHeight: number;
47
}
48
interface PlacementArgs {
49
  maxHeight: number;
50
  menuEl: HTMLDivElement | null;
51
  minHeight: number;
52
  placement: MenuPlacement;
53
  shouldScroll: boolean;
54
  isFixedPosition: boolean;
55
  controlHeight: number;
56
}
57

58
export function getMenuPlacement({
59
  maxHeight: preferredMaxHeight,
60
  menuEl,
61
  minHeight,
62
  placement: preferredPlacement,
63
  shouldScroll,
64
  isFixedPosition,
65
  controlHeight,
66
}: PlacementArgs): CalculatedMenuPlacementAndHeight {
67
  const scrollParent = getScrollParent(menuEl!);
169✔
68
  const defaultState: CalculatedMenuPlacementAndHeight = {
169✔
69
    placement: 'bottom',
70
    maxHeight: preferredMaxHeight,
71
  };
72

73
  // something went wrong, return default state
74
  if (!menuEl || !menuEl.offsetParent) return defaultState;
169!
75

76
  // we can't trust `scrollParent.scrollHeight` --> it may increase when
77
  // the menu is rendered
UNCOV
78
  const { height: scrollHeight } = scrollParent.getBoundingClientRect();
×
79
  const {
80
    bottom: menuBottom,
81
    height: menuHeight,
82
    top: menuTop,
UNCOV
83
  } = menuEl.getBoundingClientRect();
×
84

UNCOV
85
  const { top: containerTop } = menuEl.offsetParent.getBoundingClientRect();
×
86
  const viewHeight = isFixedPosition
×
87
    ? window.innerHeight
88
    : normalizedHeight(scrollParent);
UNCOV
89
  const scrollTop = getScrollTop(scrollParent);
×
90

UNCOV
91
  const marginBottom = parseInt(getComputedStyle(menuEl).marginBottom, 10);
×
92
  const marginTop = parseInt(getComputedStyle(menuEl).marginTop, 10);
×
93
  const viewSpaceAbove = containerTop - marginTop;
×
94
  const viewSpaceBelow = viewHeight - menuTop;
×
95
  const scrollSpaceAbove = viewSpaceAbove + scrollTop;
×
96
  const scrollSpaceBelow = scrollHeight - scrollTop - menuTop;
×
97

UNCOV
98
  const scrollDown = menuBottom - viewHeight + scrollTop + marginBottom;
×
99
  const scrollUp = scrollTop + menuTop - marginTop;
×
100
  const scrollDuration = 160;
×
101

UNCOV
102
  switch (preferredPlacement) {
×
103
    case 'auto':
104
    case 'bottom':
105
      // 1: the menu will fit, do nothing
UNCOV
106
      if (viewSpaceBelow >= menuHeight) {
×
107
        return { placement: 'bottom', maxHeight: preferredMaxHeight };
×
108
      }
109

110
      // 2: the menu will fit, if scrolled
UNCOV
111
      if (scrollSpaceBelow >= menuHeight && !isFixedPosition) {
×
112
        if (shouldScroll) {
×
113
          animatedScrollTo(scrollParent, scrollDown, scrollDuration);
×
114
        }
115

UNCOV
116
        return { placement: 'bottom', maxHeight: preferredMaxHeight };
×
117
      }
118

119
      // 3: the menu will fit, if constrained
UNCOV
120
      if (
×
121
        (!isFixedPosition && scrollSpaceBelow >= minHeight) ||
×
122
        (isFixedPosition && viewSpaceBelow >= minHeight)
123
      ) {
UNCOV
124
        if (shouldScroll) {
×
125
          animatedScrollTo(scrollParent, scrollDown, scrollDuration);
×
126
        }
127

128
        // we want to provide as much of the menu as possible to the user,
129
        // so give them whatever is available below rather than the minHeight.
UNCOV
130
        const constrainedHeight = isFixedPosition
×
131
          ? viewSpaceBelow - marginBottom
132
          : scrollSpaceBelow - marginBottom;
133

UNCOV
134
        return {
×
135
          placement: 'bottom',
136
          maxHeight: constrainedHeight,
137
        };
138
      }
139

140
      // 4. Forked beviour when there isn't enough space below
141

142
      // AUTO: flip the menu, render above
UNCOV
143
      if (preferredPlacement === 'auto' || isFixedPosition) {
×
144
        // may need to be constrained after flipping
UNCOV
145
        let constrainedHeight = preferredMaxHeight;
×
146
        const spaceAbove = isFixedPosition ? viewSpaceAbove : scrollSpaceAbove;
×
147

UNCOV
148
        if (spaceAbove >= minHeight) {
×
149
          constrainedHeight = Math.min(
×
150
            spaceAbove - marginBottom - controlHeight,
151
            preferredMaxHeight
152
          );
153
        }
154

UNCOV
155
        return { placement: 'top', maxHeight: constrainedHeight };
×
156
      }
157

158
      // BOTTOM: allow browser to increase scrollable area and immediately set scroll
UNCOV
159
      if (preferredPlacement === 'bottom') {
×
160
        if (shouldScroll) {
×
161
          scrollTo(scrollParent, scrollDown);
×
162
        }
UNCOV
163
        return { placement: 'bottom', maxHeight: preferredMaxHeight };
×
164
      }
UNCOV
165
      break;
×
166
    case 'top':
167
      // 1: the menu will fit, do nothing
UNCOV
168
      if (viewSpaceAbove >= menuHeight) {
×
169
        return { placement: 'top', maxHeight: preferredMaxHeight };
×
170
      }
171

172
      // 2: the menu will fit, if scrolled
UNCOV
173
      if (scrollSpaceAbove >= menuHeight && !isFixedPosition) {
×
174
        if (shouldScroll) {
×
175
          animatedScrollTo(scrollParent, scrollUp, scrollDuration);
×
176
        }
177

UNCOV
178
        return { placement: 'top', maxHeight: preferredMaxHeight };
×
179
      }
180

181
      // 3: the menu will fit, if constrained
UNCOV
182
      if (
×
183
        (!isFixedPosition && scrollSpaceAbove >= minHeight) ||
×
184
        (isFixedPosition && viewSpaceAbove >= minHeight)
185
      ) {
UNCOV
186
        let constrainedHeight = preferredMaxHeight;
×
187

188
        // we want to provide as much of the menu as possible to the user,
189
        // so give them whatever is available below rather than the minHeight.
UNCOV
190
        if (
×
191
          (!isFixedPosition && scrollSpaceAbove >= minHeight) ||
×
192
          (isFixedPosition && viewSpaceAbove >= minHeight)
193
        ) {
UNCOV
194
          constrainedHeight = isFixedPosition
×
195
            ? viewSpaceAbove - marginTop
196
            : scrollSpaceAbove - marginTop;
197
        }
198

UNCOV
199
        if (shouldScroll) {
×
200
          animatedScrollTo(scrollParent, scrollUp, scrollDuration);
×
201
        }
202

UNCOV
203
        return {
×
204
          placement: 'top',
205
          maxHeight: constrainedHeight,
206
        };
207
      }
208

209
      // 4. not enough space, the browser WILL NOT increase scrollable area when
210
      // absolutely positioned element rendered above the viewport (only below).
211
      // Flip the menu, render below
UNCOV
212
      return { placement: 'bottom', maxHeight: preferredMaxHeight };
×
213
    default:
UNCOV
214
      throw new Error(`Invalid placement provided "${preferredPlacement}".`);
×
215
  }
216

UNCOV
217
  return defaultState;
×
218
}
219

220
// Menu Component
221
// ------------------------------
222

223
export interface MenuPlacementProps {
224
  /** Set the minimum height of the menu. */
225
  minMenuHeight: number;
226
  /** Set the maximum height of the menu. */
227
  maxMenuHeight: number;
228
  /** Set whether the menu should be at the top, at the bottom. The auto options sets it to bottom. */
229
  menuPlacement: MenuPlacement;
230
  /** The CSS position value of the menu, when "fixed" extra layout management is required */
231
  menuPosition: MenuPosition;
232
  /** Set whether the page should scroll to show the menu. */
233
  menuShouldScrollIntoView: boolean;
234
}
235

236
export interface MenuProps<
237
  Option = unknown,
238
  IsMulti extends boolean = boolean,
239
  Group extends GroupBase<Option> = GroupBase<Option>
240
> extends CommonPropsAndClassName<Option, IsMulti, Group>,
241
    MenuPlacementProps {
242
  /** Reference to the internal element, consumed by the MenuPlacer component */
243
  innerRef: Ref<HTMLDivElement>;
244
  innerProps: JSX.IntrinsicElements['div'];
245
  isLoading: boolean;
246
  placement: CoercedMenuPlacement;
247
  /** The children to be rendered. */
248
  children: ReactNode;
249
}
250

251
interface PlacerProps {
252
  placement: CoercedMenuPlacement;
253
  maxHeight: number;
254
}
255

256
interface ChildrenProps {
257
  ref: Ref<HTMLDivElement>;
258
  placerProps: PlacerProps;
259
}
260

261
export interface MenuPlacerProps<
262
  Option,
263
  IsMulti extends boolean,
264
  Group extends GroupBase<Option>
265
> extends CommonProps<Option, IsMulti, Group>,
266
    MenuPlacementProps {
267
  /** The children to be rendered. */
268
  children: (childrenProps: ChildrenProps) => ReactElement;
269
}
270

271
function alignToControl(placement: CoercedMenuPlacement) {
272
  const placementToCSSProp = { bottom: 'top', top: 'bottom' };
768✔
273
  return placement ? placementToCSSProp[placement] : 'bottom';
768!
274
}
275
const coercePlacement = (p: MenuPlacement) => (p === 'auto' ? 'bottom' : p);
170!
276

277
export const menuCSS = <
5✔
278
  Option,
279
  IsMulti extends boolean,
280
  Group extends GroupBase<Option>
281
>(
282
  {
283
    placement,
284
    theme: { borderRadius, spacing, colors },
285
  }: MenuProps<Option, IsMulti, Group>,
286
  unstyled: boolean
287
): CSSObjectWithLabel => ({
768✔
288
  label: 'menu',
289
  [alignToControl(placement)]: '100%',
290
  position: 'absolute',
291
  width: '100%',
292
  zIndex: 1,
293
  ...(unstyled
768!
294
    ? {}
295
    : {
296
        backgroundColor: colors.neutral0,
297
        borderRadius: borderRadius,
298
        boxShadow:
299
          '0 0 0 1px hsla(0, 0%, 0%, 0.1), 0 4px 11px hsla(0, 0%, 0%, 0.1)',
300
        marginBottom: spacing.menuGutter,
301
        marginTop: spacing.menuGutter,
302
      }),
303
});
304

305
const PortalPlacementContext =
306
  createContext<{
5✔
307
    setPortalPlacement: (placement: CoercedMenuPlacement) => void;
308
  } | null>(null);
309

310
// NOTE: internal only
311
export const MenuPlacer = <
5✔
312
  Option,
313
  IsMulti extends boolean,
314
  Group extends GroupBase<Option>
315
>(
316
  props: MenuPlacerProps<Option, IsMulti, Group>
317
) => {
318
  const {
319
    children,
320
    minMenuHeight,
321
    maxMenuHeight,
322
    menuPlacement,
323
    menuPosition,
324
    menuShouldScrollIntoView,
325
    theme,
326
  } = props;
769✔
327

328
  const { setPortalPlacement } = useContext(PortalPlacementContext) || {};
769✔
329
  const ref = useRef<HTMLDivElement | null>(null);
769✔
330
  const [maxHeight, setMaxHeight] = useState(maxMenuHeight);
769✔
331
  const [placement, setPlacement] = useState<CoercedMenuPlacement | null>(null);
769✔
332
  const { controlHeight } = theme.spacing;
769✔
333

334
  useLayoutEffect(() => {
769✔
335
    const menuEl = ref.current;
170✔
336
    if (!menuEl) return;
170✔
337

338
    // DO NOT scroll if position is fixed
339
    const isFixedPosition = menuPosition === 'fixed';
169✔
340
    const shouldScroll = menuShouldScrollIntoView && !isFixedPosition;
169✔
341

342
    const state = getMenuPlacement({
169✔
343
      maxHeight: maxMenuHeight,
344
      menuEl,
345
      minHeight: minMenuHeight,
346
      placement: menuPlacement,
347
      shouldScroll,
348
      isFixedPosition,
349
      controlHeight,
350
    });
351

352
    setMaxHeight(state.maxHeight);
169✔
353
    setPlacement(state.placement);
169✔
354
    setPortalPlacement?.(state.placement);
169✔
355
  }, [
356
    maxMenuHeight,
357
    menuPlacement,
358
    menuPosition,
359
    menuShouldScrollIntoView,
360
    minMenuHeight,
361
    setPortalPlacement,
362
    controlHeight,
363
  ]);
364

365
  return children({
769✔
366
    ref,
367
    placerProps: {
368
      ...props,
369
      placement: placement || coercePlacement(menuPlacement),
939✔
370
      maxHeight,
371
    },
372
  });
373
};
374

375
const Menu = <Option, IsMulti extends boolean, Group extends GroupBase<Option>>(
5✔
376
  props: MenuProps<Option, IsMulti, Group>
377
) => {
378
  const { children, innerRef, innerProps } = props;
768✔
379
  return (
768✔
380
    <div
381
      {...getStyleProps(props, 'menu', { menu: true })}
382
      ref={innerRef}
383
      {...innerProps}
384
    >
385
      {children}
386
    </div>
387
  );
388
};
389

390
export default Menu;
391

392
// ==============================
393
// Menu List
394
// ==============================
395

396
export interface MenuListProps<
397
  Option = unknown,
398
  IsMulti extends boolean = boolean,
399
  Group extends GroupBase<Option> = GroupBase<Option>
400
> extends CommonPropsAndClassName<Option, IsMulti, Group> {
401
  /** Set the max height of the Menu component  */
402
  maxHeight: number;
403
  /** The children to be rendered. */
404
  children: ReactNode;
405
  /** Inner ref to DOM ReactNode */
406
  innerRef: Ref<HTMLDivElement>;
407
  /** The currently focused option */
408
  focusedOption: Option;
409
  /** Props to be passed to the menu-list wrapper. */
410
  innerProps: JSX.IntrinsicElements['div'];
411
}
412
export const menuListCSS = <
5✔
413
  Option,
414
  IsMulti extends boolean,
415
  Group extends GroupBase<Option>
416
>(
417
  {
418
    maxHeight,
419
    theme: {
420
      spacing: { baseUnit },
421
    },
422
  }: MenuListProps<Option, IsMulti, Group>,
423
  unstyled: boolean
424
): CSSObjectWithLabel => ({
768✔
425
  maxHeight,
426
  overflowY: 'auto',
427
  position: 'relative', // required for offset[Height, Top] > keyboard scroll
428
  WebkitOverflowScrolling: 'touch',
429
  ...(unstyled
768!
430
    ? {}
431
    : {
432
        paddingBottom: baseUnit,
433
        paddingTop: baseUnit,
434
      }),
435
});
436
export const MenuList = <
5✔
437
  Option,
438
  IsMulti extends boolean,
439
  Group extends GroupBase<Option>
440
>(
441
  props: MenuListProps<Option, IsMulti, Group>
442
) => {
443
  const { children, innerProps, innerRef, isMulti } = props;
768✔
444
  return (
768✔
445
    <div
446
      {...getStyleProps(props, 'menuList', {
447
        'menu-list': true,
448
        'menu-list--is-multi': isMulti,
449
      })}
450
      ref={innerRef}
451
      {...innerProps}
452
    >
453
      {children}
454
    </div>
455
  );
456
};
457

458
// ==============================
459
// Menu Notices
460
// ==============================
461

462
const noticeCSS = <
5✔
463
  Option,
464
  IsMulti extends boolean,
465
  Group extends GroupBase<Option>
466
>(
467
  {
468
    theme: {
469
      spacing: { baseUnit },
470
      colors,
471
    },
472
  }: NoticeProps<Option, IsMulti, Group>,
473
  unstyled: boolean
474
): CSSObjectWithLabel => ({
38✔
475
  textAlign: 'center',
476
  ...(unstyled
38!
477
    ? {}
478
    : {
479
        color: colors.neutral40,
480
        padding: `${baseUnit * 2}px ${baseUnit * 3}px`,
481
      }),
482
});
483
export const noOptionsMessageCSS = noticeCSS;
5✔
484
export const loadingMessageCSS = noticeCSS;
5✔
485

486
export interface NoticeProps<
487
  Option = unknown,
488
  IsMulti extends boolean = boolean,
489
  Group extends GroupBase<Option> = GroupBase<Option>
490
> extends CommonPropsAndClassName<Option, IsMulti, Group> {
491
  /** The children to be rendered. */
492
  children: ReactNode;
493
  /** Props to be passed on to the wrapper. */
494
  innerProps: JSX.IntrinsicElements['div'];
495
}
496

497
export const NoOptionsMessage = <
5✔
498
  Option,
499
  IsMulti extends boolean,
500
  Group extends GroupBase<Option>
501
>({
502
  children = 'No options',
×
503
  innerProps,
504
  ...restProps
505
}: NoticeProps<Option, IsMulti, Group>) => {
506
  return (
30✔
507
    <div
508
      {...getStyleProps(
509
        { ...restProps, children, innerProps },
510
        'noOptionsMessage',
511
        {
512
          'menu-notice': true,
513
          'menu-notice--no-options': true,
514
        }
515
      )}
516
      {...innerProps}
517
    >
518
      {children}
519
    </div>
520
  );
521
};
522

523
export const LoadingMessage = <
5✔
524
  Option,
525
  IsMulti extends boolean,
526
  Group extends GroupBase<Option>
527
>({
528
  children = 'Loading...',
×
529
  innerProps,
530
  ...restProps
531
}: NoticeProps<Option, IsMulti, Group>) => {
532
  return (
8✔
533
    <div
534
      {...getStyleProps(
535
        { ...restProps, children, innerProps },
536
        'loadingMessage',
537
        {
538
          'menu-notice': true,
539
          'menu-notice--loading': true,
540
        }
541
      )}
542
      {...innerProps}
543
    >
544
      {children}
545
    </div>
546
  );
547
};
548

549
// ==============================
550
// Menu Portal
551
// ==============================
552

553
export interface MenuPortalProps<
554
  Option,
555
  IsMulti extends boolean,
556
  Group extends GroupBase<Option>
557
> extends CommonPropsAndClassName<Option, IsMulti, Group> {
558
  appendTo: HTMLElement | undefined;
559
  children: ReactNode; // ideally Menu<MenuProps>
560
  controlElement: HTMLDivElement | null;
561
  innerProps: JSX.IntrinsicElements['div'];
562
  menuPlacement: MenuPlacement;
563
  menuPosition: MenuPosition;
564
}
565

566
export interface PortalStyleArgs {
567
  offset: number;
568
  position: MenuPosition;
569
  rect: { left: number; width: number };
570
}
571

572
export const menuPortalCSS = ({
5✔
573
  rect,
574
  offset,
575
  position,
UNCOV
576
}: PortalStyleArgs): CSSObjectWithLabel => ({
×
577
  left: rect.left,
578
  position: position,
579
  top: offset,
580
  width: rect.width,
581
  zIndex: 1,
582
});
583

584
interface ComputedPosition {
585
  offset: number;
586
  rect: { left: number; width: number };
587
}
588

589
export const MenuPortal = <
5✔
590
  Option,
591
  IsMulti extends boolean,
592
  Group extends GroupBase<Option>
593
>(
594
  props: MenuPortalProps<Option, IsMulti, Group>
595
) => {
596
  const {
597
    appendTo,
598
    children,
599
    controlElement,
600
    innerProps,
601
    menuPlacement,
602
    menuPosition,
UNCOV
603
  } = props;
×
604

UNCOV
605
  const menuPortalRef = useRef<HTMLDivElement | null>(null);
×
606
  const cleanupRef = useRef<(() => void) | void | null>(null);
×
607

UNCOV
608
  const [placement, setPortalPlacement] = useState<'bottom' | 'top'>(
×
609
    coercePlacement(menuPlacement)
610
  );
UNCOV
611
  const portalPlacementContext = useMemo(
×
612
    () => ({
×
613
      setPortalPlacement,
614
    }),
615
    []
616
  );
617
  const [computedPosition, setComputedPosition] =
UNCOV
618
    useState<ComputedPosition | null>(null);
×
619

UNCOV
620
  const updateComputedPosition = useCallback(() => {
×
621
    if (!controlElement) return;
×
622

UNCOV
623
    const rect = getBoundingClientObj(controlElement);
×
624
    const scrollDistance = menuPosition === 'fixed' ? 0 : window.pageYOffset;
×
625
    const offset = rect[placement] + scrollDistance;
×
626
    if (
×
627
      offset !== computedPosition?.offset ||
×
628
      rect.left !== computedPosition?.rect.left ||
629
      rect.width !== computedPosition?.rect.width
630
    ) {
UNCOV
631
      setComputedPosition({ offset, rect });
×
632
    }
633
  }, [
634
    controlElement,
635
    menuPosition,
636
    placement,
637
    computedPosition?.offset,
638
    computedPosition?.rect.left,
639
    computedPosition?.rect.width,
640
  ]);
641

UNCOV
642
  useLayoutEffect(() => {
×
643
    updateComputedPosition();
×
644
  }, [updateComputedPosition]);
645

UNCOV
646
  const runAutoUpdate = useCallback(() => {
×
647
    if (typeof cleanupRef.current === 'function') {
×
648
      cleanupRef.current();
×
649
      cleanupRef.current = null;
×
650
    }
651

UNCOV
652
    if (controlElement && menuPortalRef.current) {
×
653
      cleanupRef.current = autoUpdate(
×
654
        controlElement,
655
        menuPortalRef.current,
656
        updateComputedPosition,
657
        { elementResize: 'ResizeObserver' in window }
658
      );
659
    }
660
  }, [controlElement, updateComputedPosition]);
661

UNCOV
662
  useLayoutEffect(() => {
×
663
    runAutoUpdate();
×
664
  }, [runAutoUpdate]);
665

UNCOV
666
  const setMenuPortalElement = useCallback(
×
667
    (menuPortalElement: HTMLDivElement) => {
UNCOV
668
      menuPortalRef.current = menuPortalElement;
×
669
      runAutoUpdate();
×
670
    },
671
    [runAutoUpdate]
672
  );
673

674
  // bail early if required elements aren't present
UNCOV
675
  if ((!appendTo && menuPosition !== 'fixed') || !computedPosition) return null;
×
676

677
  // same wrapper element whether fixed or portalled
678
  const menuWrapper = (
UNCOV
679
    <div
×
680
      ref={setMenuPortalElement}
681
      {...getStyleProps(
682
        {
683
          ...props,
684
          offset: computedPosition.offset,
685
          position: menuPosition,
686
          rect: computedPosition.rect,
687
        },
688
        'menuPortal',
689
        {
690
          'menu-portal': true,
691
        }
692
      )}
693
      {...innerProps}
694
    >
695
      {children}
696
    </div>
697
  );
698

UNCOV
699
  return (
×
700
    <PortalPlacementContext.Provider value={portalPlacementContext}>
701
      {appendTo ? createPortal(menuWrapper, appendTo) : menuWrapper}
×
702
    </PortalPlacementContext.Provider>
703
  );
704
};
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

© 2025 Coveralls, Inc