All files / src/components expandable-list.tsx

100% Statements 24/24
85.29% Branches 29/34
100% Functions 4/4
100% Lines 24/24

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 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154                                          8x             11x 11x 8x 6x   2x       8x   11x       6x                                                                             8x                   8x       8x   8x 2x     6x 6x           31x 31x 31x   31x       6x 6x 5x                                       6x                            
import {
  type FC,
  Children,
  useState,
  type ReactNode,
  type HTMLAttributes,
} from 'react';
import cn from 'classnames';
 
import Button from './button';
 
import '../styles/components/expandable-list.scss';
 
type ExpandableMessageProps = {
  expanded?: boolean;
  setExpanded: (expanded: boolean) => unknown;
  descriptionString?: string;
  showHideWording?: boolean;
  nHiddenItems?: number;
};
 
export const ExpandableMessage: FC<ExpandableMessageProps> = ({
  descriptionString = 'items',
  expanded,
  setExpanded,
  showHideWording,
  nHiddenItems,
}) => {
  let message = `${showHideWording ? 'Hide' : 'Fewer'} ${descriptionString}`;
  if (!expanded) {
    if (nHiddenItems === undefined) {
      message = showHideWording ? 'Show' : 'More';
    } else {
      message = showHideWording
        ? `Show ${nHiddenItems}`
        : `${nHiddenItems} more`;
    }
    message += ` ${descriptionString}`;
  }
  return (
    <Button
      variant="tertiary"
      className="expandable-list__action"
      onClick={() => setExpanded(!expanded)}
      data-testid="expandable-message"
    >
      {message}
    </Button>
  );
};
 
type ExpandableListProps = {
  /**
   * Children as an array of react elements, items of the list
   */
  children?: ReactNode;
  /**
   * Threshold from which to start hiding items of the list
   */
  numberCollapsedItems?: number;
  /**
   * Description of the items to put in text of the open/close button
   */
  descriptionString?: string;
  /**
   * Wether to show or hide the visual bullet points
   */
  showBullets?: boolean;
  /**
   * Extra element to place alongside the open/close button
   */
  extraActions?: ReactNode;
  /**
   * Wether to display or not the number of hidden elements
   */
  displayNumberOfHiddenItems?: boolean;
  /**
   * Classnames to be added to the list container
   */
  className?: string;
};
 
export const ExpandableList = ({
  children: c,
  numberCollapsedItems = 5,
  descriptionString = 'items',
  showBullets,
  extraActions,
  displayNumberOfHiddenItems,
  className,
  ...props
}: ExpandableListProps & HTMLAttributes<HTMLUListElement>) => {
  const [expanded, setExpanded] = useState(false);
 
  // get an array of children, filter out null or undefined children to avoid
  // counting them towards the threshold limit
  const children = Children.toArray(c).filter(Boolean);
 
  if (!children.length) {
    return null;
  }
 
  const enoughChildren = children.length > numberCollapsedItems + 1;
  const itemNodes = Children.map(
    children.slice(
      0,
      expanded || !enoughChildren ? children.length : numberCollapsedItems
    ),
    (child, index) => {
      let key: string | number = index;
      Eif (typeof child === 'object' && 'key' in child && child.key) {
        key = child.key;
      }
      return <li key={key}>{child}</li>;
    }
  );
 
  let actions = null;
  if (enoughChildren || extraActions) {
    actions = (
      <li>
        {enoughChildren && (
          <ExpandableMessage
            expanded={expanded}
            setExpanded={setExpanded}
            descriptionString={descriptionString || 'items'}
            showHideWording={numberCollapsedItems === 0}
            nHiddenItems={
              displayNumberOfHiddenItems
                ? children.length - numberCollapsedItems
                : undefined
            }
          />
        )}
        {extraActions}
      </li>
    );
  }
 
  return (
    <ul
      className={cn(className, 'expandable-list', {
        'no-bullet': !showBullets,
      })}
      {...props}
    >
      {itemNodes}
      {actions}
    </ul>
  );
};
 
export default ExpandableList;