Source: select.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, { useEffect, useRef } from 'react';
import { MDCSelect } from '@material/select';
import { useMDCComponent, useMDCEvent } from './hooks';

const generateRootClassName = (props) => {
  const rootClassNames = ['mdc-select'];
  switch (props.variation || 'filled') {
    case 'filled':
      rootClassNames.push('mdc-select--filled');
      break;
    case 'outlined':
      rootClassNames.push('mdc-select--outlined');
      break;
    default:
      throw new Error(`Not suported variation; ${props.variation}`);
  }
  if (props.required) {
    rootClassNames.push('mdc-select--required');
  }
  if (props.disabled) {
    rootClassNames.push('mdc-select--disabled');
  }
  if (props.className) {
    rootClassNames.push(props.className);
  }
  return rootClassNames.join(' ');
};

const getItemValue = (item, itemsValueAttr, index) => {
  if (itemsValueAttr) {
    return item[itemsValueAttr] || '';
  }
  return typeof item === 'object' ? index.toString() : item.toString();
};

/**
 * [MDCSelect component]{@link https://github.com/material-components/material-components-web/tree/master/packages/mdc-select#readme}
 * implemented by react component.
 * @function Select
 * @param {Object} props
 * @param {Object[]|string[]} props.items The list of options.
 * @param {string} [props.itemsValueAttr] The attribute name of the options to hold a value.
 * If not specified, index of items is used.
 * @param {string} [props.itemsTextAttr] The attribute name of the options to show contents
 * of the option. If not specified, the return value of `item.toString()` is used.
 * @param {string} [props.value] The value of `props.itemsValueAttr` attributes of the
 * selected option.
 * @param {string} [props.variation] The variation of the select. Supported variations are
 * `'filled'`(default) and `'outlined'`.
 * @param {string} [props.label] The label text of the select.
 * @param {string} [props.className] The class name that is added to the root element.
 * @param {boolean} [props.required] Specifies `true` if the select is required.
 * Default to `false`.
 * @param {boolean} [props.disabled] Specifies `true` if you want to disable the select.
 * Default to `false`.
 * @param {string} [props.name] Specifying a value for this property, the component generate
 * an `<input type="hidden" name="{value of property}">` elelment. A value of the element
 * is synchronized with value of Select, you can use the component as-is HTML form.
 * @param {React.MutableRefObject} [props.mdcSelectRef] MutableRefObject which bind an
 * MDCSelect instance to.
 * @param {EventHandler} [props.onChange] Specifies event handler that is called when
 * a option has been selected.
 * @returns {DetailedReactHTMLElement}
 * @exports material-react-js
 */
export default function Select(props) {
  const rootElementRef = useRef();
  const mdcSelectRef = useMDCComponent(
    MDCSelect,
    rootElementRef,
    props.mdcSelectRef,
  );

  useEffect(() => {
    mdcSelectRef.current.foundation.setValue(props.value, true);
    mdcSelectRef.current.foundation.layout();
  }, [mdcSelectRef, props.value]);

  useMDCEvent(mdcSelectRef, 'MDCSelect:change', props.onChange);

  const selectedItemText = props.value == null
    ? ''
    : props.items.reduce((text, item, i) => {
      if (text) {
        return text;
      }
      const itemValue = getItemValue(item, props.itemsValueAttr, i);
      if (props.value !== itemValue) {
        return text;
      }
      return props.itemsTextAttr ? item[props.itemsTextAttr] : item.toString();
    }, '');

  return (
    <div className={generateRootClassName(props)} ref={rootElementRef}>
      {props.name && <input type="hidden" name={props.name} value={selectedItemText}/>}
      <div className="mdc-select__anchor"
           role="button"
           aria-haspopup="listbox"
           aria-required={props.required ? 'true' : null}
           aria-disabled={props.disabled ? 'true' : null}>
        {props.variation === 'outlined'
          ? (
            <span className="mdc-notched-outline">
              <span className="mdc-notched-outline__leading"></span>
              {props.label && (
              <span className="mdc-notched-outline__notch">
                <span className="mdc-floating-label">{props.label}</span>
              </span>
              )}
              <span className="mdc-notched-outline__trailing"></span>
            </span>
          )
          : (
            <>
              <span className="mdc-select__ripple"></span>
              {props.label && <span className="mdc-floating-label">{props.label}</span>}
            </>
          )}
        <span className="mdc-select__selected-text-container">
          <span className="mdc-select__selected-text">{selectedItemText}</span>
        </span>
        <span className="mdc-select__dropdown-icon">
          <svg className="mdc-select__dropdown-icon-graphic" viewBox="7 10 10 5" focusable="false">
            <polygon className="mdc-select__dropdown-icon-inactive"
                     stroke="none"
                     fillRule="evenodd"
                     points="7 10 12 15 17 10"/>
            <polygon className="mdc-select__dropdown-icon-active"
                     stroke="none"
                     fillRule="evenodd"
                     points="7 15 12 10 17 15"/>
          </svg>
        </span>
        {props.variation === 'outlined' ? '' : <span className="mdc-line-ripple"></span>}
      </div>
      <div className="mdc-select__menu mdc-menu mdc-menu-surface mdc-menu-surface--fullwidth">
        <ul className="mdc-list" role="listbox">
        {props.items.map((item, i) => {
          const itemValue = getItemValue(item, props.itemsValueAttr, i);
          const text = props.itemsTextAttr ? item[props.itemsTextAttr] : item.toString();
          const selected = props.value && (props.value === itemValue);
          return (
            <li className={selected ? 'mdc-list-item mdc-list-item--selected' : 'mdc-list-item'}
                data-value={itemValue}
                role="option"
                aria-selected={selected ? 'true' : null}
                key={itemValue}>
              <span className="mdc-list-item__ripple"></span>
              {text && <span className="mdc-list-item__text">{text}</span>}
            </li>
          );
        })}
        </ul>
      </div>
    </div>
  );
}