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

JedWatson / react-select / 9de42ca4-8c55-4031-8310-e7e0a2b0128b

26 Oct 2024 11:27AM CUT coverage: 75.844%. Remained the same
9de42ca4-8c55-4031-8310-e7e0a2b0128b

Pull #5880

circleci

lukebennett88
add box-sizing to border-box for RequiredInput

adding `required` would otherwise cause an extra (unstylable) component to be added which has some implicit padding from the user agent style sheet (inputs have padding) which could cause horizontal scrolling when the whole scroll field is 100% wide.
Pull Request #5880: add box-sizing to border-box for RequiredInput

658 of 1052 branches covered (62.55%)

1033 of 1362 relevant lines covered (75.84%)

1934.69 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
78
  const { height: scrollHeight } = scrollParent.getBoundingClientRect();
×
79
  const {
80
    bottom: menuBottom,
81
    height: menuHeight,
82
    top: menuTop,
83
  } = menuEl.getBoundingClientRect();
×
84

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

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

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

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

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

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

119
      // 3: the menu will fit, if constrained
120
      if (
×
121
        (!isFixedPosition && scrollSpaceBelow >= minHeight) ||
×
122
        (isFixedPosition && viewSpaceBelow >= minHeight)
123
      ) {
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.
130
        const constrainedHeight = isFixedPosition
×
131
          ? viewSpaceBelow - marginBottom
132
          : scrollSpaceBelow - marginBottom;
133

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
143
      if (preferredPlacement === 'auto' || isFixedPosition) {
×
144
        // may need to be constrained after flipping
145
        let constrainedHeight = preferredMaxHeight;
×
146
        const spaceAbove = isFixedPosition ? viewSpaceAbove : scrollSpaceAbove;
×
147

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

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

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

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

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

181
      // 3: the menu will fit, if constrained
182
      if (
×
183
        (!isFixedPosition && scrollSpaceAbove >= minHeight) ||
×
184
        (isFixedPosition && viewSpaceAbove >= minHeight)
185
      ) {
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.
190
        if (
×
191
          (!isFixedPosition && scrollSpaceAbove >= minHeight) ||
×
192
          (isFixedPosition && viewSpaceAbove >= minHeight)
193
        ) {
194
          constrainedHeight = isFixedPosition
×
195
            ? viewSpaceAbove - marginTop
196
            : scrollSpaceAbove - marginTop;
197
        }
198

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

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
212
      return { placement: 'bottom', maxHeight: preferredMaxHeight };
×
213
    default:
214
      throw new Error(`Invalid placement provided "${preferredPlacement}".`);
×
215
  }
216

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,
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,
603
  } = props;
×
604

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

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

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

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
    ) {
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

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

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

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

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

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

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

677
  // same wrapper element whether fixed or portalled
678
  const menuWrapper = (
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

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