All files / src/components data-list.tsx

80% Statements 12/15
20% Branches 1/5
100% Functions 4/4
80% Lines 12/15

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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                                                                                        8x               40x 40x                           40x   8x   8x                 9x   9x             90x 90x                             8x   6x  
import { memo, type ReactNode, type HTMLAttributes } from 'react';
import cn from 'classnames';
 
import Loader from './loader';
 
import useDataCheckboxes from '../hooks/useDataCheckboxes';
 
import withDataLoader, { type WrapperProps } from './data-loader';
 
export type Props<Datum> = {
  /**
   * The data to be displayed
   */
  data: Datum[];
  /**
   * Flag saying that data is loading, so we might be showing stale data
   */
  loading?: boolean;
  /**
   * A function that returns a unique ID for each of the data objects.
   * Same function signature as a map function.
   */
  getIdKey: (datum: Datum, index: number, data: Datum[]) => string;
  /**
   * A callback that is called whenever a user selects or unselects a row.
   */
  onSelectionChange?: (event: MouseEvent | KeyboardEvent) => void;
  /**
   * A renderer function for each item of the list.
   * Make sure that it doesn't change unecessarily by wrapping it in useCallback
   */
  dataRenderer: (datum: Datum) => ReactNode;
};
 
type BasicDatum = Record<string, unknown>;
 
type DataListItemProps<Datum> = {
  datum: Datum;
  id: string;
  dataRenderer: (datum: Datum) => ReactNode;
  loading: boolean;
  firstItem: boolean;
};
 
const DataListItem = <Datum extends BasicDatum>({
  datum,
  id,
  dataRenderer,
  loading,
  firstItem,
}: DataListItemProps<Datum>) => {
  let rendered: ReactNode;
  try {
    rendered = dataRenderer(datum);
  } catch (error) {
    /**
     * We get here only if the renderer fails. If the renderer returns null of
     * undefined because of a lack of data, then it will no throw and will not
     * display the loader at all
     */
    if (!loading) {
      throw error;
    } else {
      rendered = firstItem && <Loader />;
    }
  }
 
  return <li key={id}>{rendered}</li>;
};
const MemoizedDataListItem = memo(DataListItem) as typeof DataListItem;
 
export const DataList = <Datum extends BasicDatum>({
  data,
  getIdKey,
  dataRenderer,
  loading = false,
  onSelectionChange,
  className,
  ...props
}: Props<Datum> & HTMLAttributes<HTMLUListElement>) => {
  const { checkboxContainerRef } = useDataCheckboxes(onSelectionChange);
 
  return (
    <ul
      {...props}
      className={cn('data-list', 'no-bullet', className)}
      ref={checkboxContainerRef}
    >
      {data.map((datum, index) => {
        const id = getIdKey(datum, index, data);
        return (
          <MemoizedDataListItem
            key={id}
            datum={datum}
            id={id}
            dataRenderer={dataRenderer}
            loading={loading}
            firstItem={index === 0}
          />
        );
      })}
    </ul>
  );
};
 
export const DataListWithLoader = <Datum extends BasicDatum>(
  props: WrapperProps<Datum> & Props<Datum> & HTMLAttributes<HTMLElement>
) => <>{withDataLoader<Datum, typeof props>(DataList)(props)}</>;