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 };