Source: data-table.jsx

/**
 * @license
 * Copyright 2020 Restus Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

import React, { useRef, useCallback } from 'react';
import { MDCDataTable } from '@material/data-table';
import Checkbox from './checkbox';
import { useMDCEvent, useMDCComponent } from './hooks';

const headerCellClassName = (props) => {
  const classList = ['mdc-data-table__header-cell'];
  if (props.isNumeric) {
    classList.push('mdc-data-table__header-cell--numeric');
  }
  if (props.className) {
    classList.push(props.className);
  }
  if (props.isSortable) {
    classList.push('mdc-data-table__header-cell--with-sort');
    if (props.sortStatus) {
      classList.push('mdc-data-table__header-cell--sorted');
      if (props.sortStatus === 'descending') {
        classList.push('mdc-data-table__header-cell--sorted-descending');
      }
    }
  }
  return classList.join(' ');
};

function HeaderContent(props) {
  if (props.isSortable) {
    return (
      <div className="mdc-data-table__header-cell-wrapper">
        {!props.isNumeric && <div className="mdc-data-table__header-cell-label">{props.children}</div>}
        <button className="mdc-icon-button material-icons mdc-data-table__sort-icon-button">arrow_upward</button>
        {props.isNumeric && <div className="mdc-data-table__header-cell-label">{props.children}</div>}
        <div className="mdc-data-table__sort-status-label" aria-hidden="true"></div>
      </div>
    );
  }
  return props.children || '';
}

function HeaderCell(props) {
  const attrs = {
    className: headerCellClassName(props),
    role: 'columnheader',
    scope: 'col',
    ...(props.isSortable ? {
      'aria-sort': props.sortStatus || 'none',
      'data-column-id': props.name,
    } : {}),
    ...(props.attrs || {}),
  };
  return (
    <th {...attrs}>
      <HeaderContent {...props}/>
    </th>
  );
}

const cellClassName = (className, isNumeric) => {
  const classList = ['mdc-data-table__cell'];
  if (isNumeric) {
    classList.push('mdc-data-table__cell--numeric');
  }
  if (className) {
    classList.push(className);
  }
  return classList.join(' ');
};

const getContent = (content, rowData) => {
  if (typeof content === 'string') {
    return content.split('.').reduce((obj, accessor) => obj && obj[accessor], rowData);
  }
  if (typeof content === 'function') {
    return content(rowData);
  }
  return '';
};

function Cell(props) {
  const attrs = {
    className: cellClassName(props.className, props.isNumeric),
    ...(props.isRowHeader ? { scope: 'row' } : {}),
    ...(props.attrs && Object.entries(props.attrs).reduce((obj, [key, value]) => (
      Object.assign(obj, { [key]: getContent(value, props.rowData) })
    ), {})),
  };
  if (props.isRowHeader) {
    return (
      <th {...attrs}>
        {props.content ? getContent(props.content, props.rowData) : props.children}
      </th>
    );
  }
  return (
    <td {...attrs}>
      {props.content ? getContent(props.content, props.rowData) : props.children}
    </td>
  );
}

/**
 * [MDCDataTable component]{@link https://github.com/material-components/material-components-web/tree/master/packages/mdc-data-table#readme}
 * implemented by react component. This component can be used in combination with
 * [Pagenation]{@link module:Pagination}.
 * @function DataTable
 * @param {Object} props
 * @param {Object[]} props.data The data source of the table body contents.
 * @param {string} [props.keyField] The property name of data source used to uniquely
 * identify the data source an element. A nested property can be specified by connecting
 * then with `'.'`.
 * @param {string} [props.className] The class name that is added to the root element.
 * @param {string} [props.tableClassName] The class name that is added to the HTML `table`
 * element.
 * @param {boolean} [props.omitsHeaderRow] `true` if not display header row, otherwise `false`.
 * Default to `false`.
 * @param {Object[]} props.columns The setting of data table's columns.
 * @param {string} [props.columns[].key] The identifier for uniquely identifying the column.
 * @param {string} [props.columns[].header] The header content of the column.
 * @param {string|BodyRenderer} props.columns[].content The property name of the data source
 * used to get the content of the table body. A nested property can be specified by connecting
 * then with `'.'`.
 * @param {boolean} [props.columns[].isNumeric] Specify `true` if this columns is a numeric
 * column, otherwise `false`. Default to `false`.
 * @param {boolean} [props.columns[].isRowHeader] Specify `true` if a cells of this columns
 * is a header for each row, otherwise `false`. Default to `false`.
 * @param {string} [props.columns[].className] The class name that is added to cells of the
 * column.
 * @param {string} [props.columns[].headerClassName] The class name that is added to a cell
 * of the column in the header row.
 * @param {string} [props.columns[].bodyClassName] The class name that is added to cells
 * of the column in the body rows.
 * @param {Object} [props.columns[].headerAttrs] The attributes to add to cell elements
 * in the header row. Specify an object that has the attribute name as the key and the attribute
 * value (`string`) as the value.
 * @param {Object} [props.columns[].bodyAttrs] The attributes to add to cell elements
 * in the body rows. Specify an object that has the attribute name as the key and the attribute
 * value (`string` or  {@link BodyRenderer}) as the value. The format of the values is the same
 * as the `content` property.
 * @param {boolean} [props.columns[].isSortable] Specify `true` if the column is sortable,
 * otherwise `false`. Default to `false`.
 * @param {string} [props.columns[].sortStatus] Specify `'ascending'` or `'descending'`
 * if the column is sorted. If `props.columns[].isSortable` is `false`, this attribute is
 * ignored.
 * @param {boolean} [props.usesRowSelection] Specify `true` if table has the row selection
 * feature, otherwise `false`. Default to `false`.
 * @param {string} [props.selectionField] The property name of data source used to select
 * a row in the table. A nested property can be specified by connecting then with `'.'`.
 * @param {RowClassName} [props.rowClassName] The function to get the class name of
 * the table row.
 * @param {string} [props.aria-label] The `aria-label` attribute added to the table tag.
 * @param {boolean} [props.usesStickyHeader] Specify `true` if you want to make header
 * row sticky (fixed) on vertical scroll, otherwise `false`. Default to `false`. (Note:
 * Sticky header feature is not compatible with IE11 browsers.)
 * @param {React.MutableRefObject} [props.mdcDataTableRef] MutableRefObject which bind an
 * MDCDataTable instance to. The instance is bind only if `props.usesRowSelection` is
 * `true` or `props.columns` includes some sortable columns.
 * @param {EventHandler} [props.onScroll] Specifies event handler that is called when
 * table is scrolled.
 * @param {EventHandler} [props.onRowSelectionChanged] Specifies event handler that is
 * called when row selection checkbox is clicked.
 * @param {EventHandler} [props.onSelectedAll] Specifies event handler that is called
 * when all rows are selected by clicking checkbox in header.
 * @param {EventHandler} [props.onUnselectedAll] Specifies event handler that is called
 * when all rows are unselected by clicking checkbox in header.
 * @param {EventHandler} [props.onSorted] Specifies event handler that is called when
 * sort icon of header cell is clicked.
 * @returns {DetailedReactHTMLElement}
 * @exports material-react-js
 */
export default function DataTable(props) {
  const sortable = props.columns.some((column) => column.isSortable);
  const rootElementRef = useRef();
  const mdcDataTableRef = useMDCComponent(
    MDCDataTable,
    rootElementRef,
    props.mdcDataTableRef,
    !sortable && !props.usesRowSelection,
  );

  useMDCEvent(
    mdcDataTableRef,
    'MDCDataTable:rowSelectionChanged',
    props.usesRowSelection && props.onRowSelectionChanged,
  );
  useMDCEvent(
    mdcDataTableRef,
    'MDCDataTable:selectedAll',
    props.usesRowSelection && props.onSelectedAll,
  );
  useMDCEvent(
    mdcDataTableRef,
    'MDCDataTable:unselectedAll',
    props.usesRowSelection && props.onUnselectedAll,
  );
  useMDCEvent(
    mdcDataTableRef,
    'MDCDataTable:sorted',
    sortable && props.onSorted,
  );
  const onRowSelectionChanged = useCallback((rowIndex, rowId, event) => {
    if (mdcDataTableRef.current) {
      mdcDataTableRef.current.foundation.adapter.notifyRowSelectionChanged({
        rowIndex, rowId, selected: event.target.checked,
      });
    }
  }, [mdcDataTableRef]);

  const rootClassList = ['mdc-data-table'];
  if (props.usesStickyHeader) {
    rootClassList.push('mdc-data-table--sticky-header');
  }
  if (props.className) {
    rootClassList.push(props.className);
  }

  return (
    <div className={rootClassList.join(' ')} ref={rootElementRef}>
      <div className="mdc-data-table__table-container" onScroll={props.onScroll}>
        <table className={props.tableClassName ? `mdc-data-table__table ${props.tableClassName}` : 'mdc-data-table__table'} aria-label={props['aria-label']}>
        {props.omitsHeaderRow
          ? null
          : (
            <thead>
              <tr className="mdc-data-table__header-row">
              {props.usesRowSelection && (
                <HeaderCell className="mdc-data-table__header-cell--checkbox">
                  <Checkbox className="mdc-data-table__header-row-checkbox"
                            disablesMdcInstance={true}
                            aria-label="Toggle all rows"/>
                </HeaderCell>
              )}
              {props.columns.map((column, j) => (
                <HeaderCell className={[column.className, column.headerClassName].join(' ').trim()}
                            isNumeric={column.isNumeric}
                            data={props.data}
                            isSortable={column.isSortable}
                            name={column.key}
                            sortStatus={column.sortStatus}
                            attrs={column.headerAttrs}
                            key={column.key || j}>
                  {column.header}
                </HeaderCell>
              ))}
              </tr>
            </thead>
          )}
          <tbody className="mdc-data-table__content">
          {props.data.map((rowData, i) => {
            const key = props.keyField
              ? props.keyField.split('.').reduce((obj, accessor) => obj && obj[accessor], rowData)
              : i;
            const rowClassNames = ['mdc-data-table__row'];
            const selected = props.usesRowSelection && getContent(props.selectionField, rowData);
            if (selected) {
              rowClassNames.push('mdc-data-table__row--selected');
            }
            const additionalRowClassName = props.rowClassName && props.rowClassName(rowData);
            if (additionalRowClassName) {
              rowClassNames.push(additionalRowClassName);
            }
            return (
              <tr className={rowClassNames.join(' ')} key={key}
                  {...(props.usesRowSelection ? { 'data-row-id': key } : {})}>
              {props.usesRowSelection && (
                <Cell className="mdc-data-table__cell--checkbox">
                  <Checkbox className="mdc-data-table__row-checkbox"
                            checked={Boolean(selected)}
                            disablesMdcInstance={true}
                            onChange={onRowSelectionChanged.bind(null, i, key)}/>
                </Cell>
              )}
              {props.columns.map((column, j) => (
                <Cell className={[column.className, column.bodyClassName].join(' ').trim()}
                      isNumeric={column.isNumeric}
                      content={column.content}
                      isRowHeader={column.isRowHeader}
                      rowData={rowData}
                      attrs={column.bodyAttrs}
                      key={column.key || j}/>
              ))}
              </tr>
            );
          })}
          </tbody>
        </table>
      </div>
      {props.children}
    </div>
  );
}

/**
 * The function that return the content of a cell in a body row.
 * @typedef {Function} BodyRenderer
 * @param {Object} rowData The data source element for the target row.
 * @returns {string|DetailedReactHTMLElement} The content of a body cell.
 */

/**
 * The function that return class name of a body row.
 * @typedef {Function} RowClassName
 * @param {Object} rowData The data source element for the target row.
 * @returns {string} The class name of a body row.
 */