vitess.io/vitess@v0.16.2/web/vtadmin/src/components/inputs/Select.tsx (about)

     1  /**
     2   * Copyright 2021 The Vitess Authors.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  import cx from 'classnames';
    17  import { useSelect, UseSelectStateChange } from 'downshift';
    18  import * as React from 'react';
    19  
    20  import { Label } from './Label';
    21  import style from './Select.module.scss';
    22  import { Icon, Icons } from '../Icon';
    23  
    24  interface Props<T> {
    25      className?: string;
    26      disabled?: boolean;
    27      inputClassName?: string;
    28      items: T[];
    29      itemToString?: (item: T | null) => string;
    30      label: string;
    31      onChange: (selectedItem: T | null | undefined) => void;
    32      placeholder: string;
    33      emptyPlaceholder?: string | (() => JSX.Element | string);
    34      renderItem?: (item: T) => JSX.Element | string;
    35      selectedItem: T | null;
    36      size?: 'large';
    37      description?: string;
    38      required?: boolean;
    39  }
    40  
    41  /**
    42   * Select performs exactly the same as the native HTML <select> in terms
    43   * of accessibility and functionality... but it looks much prettier,
    44   * and allows for fine-grained rendering control. :)
    45   */
    46  export const Select = <T,>({
    47      className,
    48      disabled,
    49      inputClassName,
    50      itemToString,
    51      items,
    52      label,
    53      onChange,
    54      placeholder,
    55      emptyPlaceholder,
    56      renderItem,
    57      selectedItem,
    58      size,
    59      description,
    60      required,
    61  }: Props<T>) => {
    62      const _itemToString = React.useCallback(
    63          (item: T | null): string => {
    64              if (typeof itemToString === 'function') return itemToString(item);
    65              return item ? String(item) : '';
    66          },
    67          [itemToString]
    68      );
    69  
    70      const onSelectedItemChange = React.useCallback(
    71          (changes: UseSelectStateChange<T>) => {
    72              onChange(changes.selectedItem);
    73          },
    74          [onChange]
    75      );
    76  
    77      const {
    78          getItemProps,
    79          getLabelProps,
    80          getMenuProps,
    81          getToggleButtonProps,
    82          highlightedIndex,
    83          isOpen,
    84          selectItem,
    85      } = useSelect({
    86          itemToString: _itemToString,
    87          items,
    88          onSelectedItemChange,
    89          selectedItem,
    90      });
    91  
    92      const containerClass = cx(style.container, className, {
    93          [style.large]: size === 'large',
    94          [style.open]: isOpen,
    95          [style.placeholder]: !selectedItem,
    96      });
    97  
    98      const _renderItem = React.useCallback(
    99          (item: T): string | JSX.Element | null => {
   100              if (typeof item === 'string') {
   101                  return item;
   102              }
   103  
   104              if (typeof renderItem === 'function') {
   105                  return renderItem(item);
   106              }
   107  
   108              return null;
   109          },
   110          [renderItem]
   111      );
   112  
   113      let content = null;
   114      if (items.length) {
   115          content = (
   116              <ul {...getMenuProps()} className={style.menu}>
   117                  {items.map((item, index) => {
   118                      const itemClass = cx({ [style.active]: highlightedIndex === index });
   119                      return (
   120                          <li key={index} className={itemClass} {...getItemProps({ item, index })}>
   121                              {_renderItem(item)}
   122                          </li>
   123                      );
   124                  })}
   125              </ul>
   126          );
   127      } else {
   128          let emptyContent = typeof emptyPlaceholder === 'function' ? emptyPlaceholder() : emptyPlaceholder;
   129          if (typeof emptyContent === 'string' || !emptyContent) {
   130              emptyContent = <div className={style.emptyPlaceholder}>{emptyContent || 'No items'}</div>;
   131          }
   132          content = (
   133              <div className={style.emptyContainer} {...getMenuProps()} data-testid="select-empty">
   134                  {emptyContent}
   135              </div>
   136          );
   137      }
   138  
   139      return (
   140          <div className={containerClass}>
   141              <Label {...getLabelProps()} label={label} required={required} />
   142              {description && <div className="mt-[-4px] mb-4">{description}</div>}
   143              <button
   144                  type="button"
   145                  {...getToggleButtonProps()}
   146                  className={cx(style.toggle, inputClassName)}
   147                  disabled={disabled}
   148              >
   149                  {selectedItem ? _renderItem(selectedItem) : placeholder}
   150                  <Icon className={style.chevron} icon={isOpen ? Icons.chevronUp : Icons.chevronDown} />
   151              </button>
   152              <div className={style.dropdown} hidden={!isOpen}>
   153                  {content}
   154                  {selectedItem && (
   155                      <button className={style.clear} onClick={() => selectItem(null as any)} type="button">
   156                          Clear selection
   157                      </button>
   158                  )}
   159              </div>
   160          </div>
   161      );
   162  };