|
|
@@ -12,6 +12,11 @@ import classNames from 'classnames'
|
|
|
import { useSelect } from 'downshift'
|
|
|
import Icon from './icon'
|
|
|
import { useTranslation } from 'react-i18next'
|
|
|
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
|
|
+import { Form, Spinner } from 'react-bootstrap-5'
|
|
|
+import FormControl from '@/features/ui/components/bootstrap-5/form/form-control'
|
|
|
+import MaterialIcon from '@/shared/components/material-icon'
|
|
|
+import { DropdownItem } from '@/features/ui/components/bootstrap-5/dropdown-menu'
|
|
|
|
|
|
export type SelectProps<T> = {
|
|
|
// The items rendered as dropdown options.
|
|
|
@@ -79,6 +84,7 @@ export const Select = <T,>({
|
|
|
getItemProps,
|
|
|
highlightedIndex,
|
|
|
openMenu,
|
|
|
+ closeMenu,
|
|
|
} = useSelect({
|
|
|
items: items ?? [],
|
|
|
itemToString,
|
|
|
@@ -117,9 +123,16 @@ export const Select = <T,>({
|
|
|
}
|
|
|
}, [name, itemToString, selectedItem, defaultItem])
|
|
|
|
|
|
+ const handleMenuKeyDown = (event: React.KeyboardEvent<HTMLUListElement>) => {
|
|
|
+ if (event.key === 'Escape' && isOpen) {
|
|
|
+ event.stopPropagation()
|
|
|
+ closeMenu()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
const onKeyDown: KeyboardEventHandler<HTMLButtonElement> = useCallback(
|
|
|
event => {
|
|
|
- if (event.key === 'Enter' && !isOpen) {
|
|
|
+ if ((event.key === 'Enter' || event.key === ' ') && !isOpen) {
|
|
|
event.preventDefault()
|
|
|
;(event.nativeEvent as any).preventDownshiftDefault = true
|
|
|
openMenu()
|
|
|
@@ -135,77 +148,148 @@ export const Select = <T,>({
|
|
|
value = defaultText
|
|
|
}
|
|
|
return (
|
|
|
- <div className="select-wrapper" ref={rootRef}>
|
|
|
- <div>
|
|
|
- {label ? (
|
|
|
- <label {...getLabelProps()}>
|
|
|
- {label}{' '}
|
|
|
- {optionalLabel && (
|
|
|
- <span className="select-optional-label text-muted">
|
|
|
- ({t('optional')})
|
|
|
- </span>
|
|
|
- )}{' '}
|
|
|
- {loading && <Icon data-testid="spinner" fw type="spinner" spin />}
|
|
|
- </label>
|
|
|
- ) : null}
|
|
|
- <div
|
|
|
- className={classNames({ disabled }, 'select-trigger')}
|
|
|
- // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
|
|
- tabIndex={0}
|
|
|
- {...getToggleButtonProps({
|
|
|
- disabled,
|
|
|
- onKeyDown,
|
|
|
- })}
|
|
|
- >
|
|
|
- <div>{value}</div>
|
|
|
+ <BootstrapVersionSwitcher
|
|
|
+ bs3={
|
|
|
+ <div className="select-wrapper" ref={rootRef}>
|
|
|
<div>
|
|
|
- {isOpen ? (
|
|
|
- <Icon type="chevron-up" fw />
|
|
|
- ) : (
|
|
|
- <Icon type="chevron-down" fw />
|
|
|
- )}
|
|
|
+ {label ? (
|
|
|
+ <label {...getLabelProps()}>
|
|
|
+ {label}{' '}
|
|
|
+ {optionalLabel && (
|
|
|
+ <span className="select-optional-label text-muted">
|
|
|
+ ({t('optional')})
|
|
|
+ </span>
|
|
|
+ )}{' '}
|
|
|
+ {loading && (
|
|
|
+ <Icon data-testid="spinner" fw type="spinner" spin />
|
|
|
+ )}
|
|
|
+ </label>
|
|
|
+ ) : null}
|
|
|
+ <div
|
|
|
+ className={classNames({ disabled }, 'select-trigger')}
|
|
|
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
|
|
+ tabIndex={0}
|
|
|
+ {...getToggleButtonProps({
|
|
|
+ disabled,
|
|
|
+ onKeyDown,
|
|
|
+ })}
|
|
|
+ >
|
|
|
+ <div>{value}</div>
|
|
|
+ <div>
|
|
|
+ {isOpen ? (
|
|
|
+ <Icon type="chevron-up" fw />
|
|
|
+ ) : (
|
|
|
+ <Icon type="chevron-down" fw />
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <ul
|
|
|
- className={classNames({ hidden: !isOpen }, 'select-items')}
|
|
|
- {...getMenuProps({ disabled })}
|
|
|
- >
|
|
|
- {isOpen &&
|
|
|
- items?.map((item, index) => {
|
|
|
- const isDisabled = itemToDisabled && itemToDisabled(item)
|
|
|
- return (
|
|
|
- <li
|
|
|
- className={classNames({
|
|
|
- 'select-highlighted': highlightedIndex === index,
|
|
|
- 'selected-active': selectedItem === item,
|
|
|
- 'select-icon': selectedIcon,
|
|
|
- 'select-disabled': isDisabled,
|
|
|
- })}
|
|
|
- key={itemToKey(item)}
|
|
|
- {...getItemProps({ item, index, disabled: isDisabled })}
|
|
|
- >
|
|
|
- <span className="select-item-title">
|
|
|
- {selectedIcon && (
|
|
|
- <div className="select-item-icon">
|
|
|
- {(selectedItem === item ||
|
|
|
- (!selectedItem && defaultItem === item)) && (
|
|
|
- <Icon type="check" fw />
|
|
|
+ <ul
|
|
|
+ className={classNames({ hidden: !isOpen }, 'select-items')}
|
|
|
+ {...getMenuProps({ disabled, onKeyDown: handleMenuKeyDown })}
|
|
|
+ >
|
|
|
+ {isOpen &&
|
|
|
+ items?.map((item, index) => {
|
|
|
+ const isDisabled = itemToDisabled && itemToDisabled(item)
|
|
|
+ return (
|
|
|
+ <li
|
|
|
+ className={classNames({
|
|
|
+ 'select-highlighted': highlightedIndex === index,
|
|
|
+ 'selected-active': selectedItem === item,
|
|
|
+ 'select-icon': selectedIcon,
|
|
|
+ 'select-disabled': isDisabled,
|
|
|
+ })}
|
|
|
+ key={itemToKey(item)}
|
|
|
+ {...getItemProps({ item, index, disabled: isDisabled })}
|
|
|
+ >
|
|
|
+ <span className="select-item-title">
|
|
|
+ {selectedIcon && (
|
|
|
+ <div className="select-item-icon">
|
|
|
+ {(selectedItem === item ||
|
|
|
+ (!selectedItem && defaultItem === item)) && (
|
|
|
+ <Icon type="check" fw />
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
)}
|
|
|
- </div>
|
|
|
- )}
|
|
|
- {itemToString(item)}
|
|
|
- </span>
|
|
|
+ {itemToString(item)}
|
|
|
+ </span>
|
|
|
|
|
|
- {itemToSubtitle ? (
|
|
|
- <span className="text-muted select-item-subtitle">
|
|
|
- {itemToSubtitle(item)}
|
|
|
- </span>
|
|
|
- ) : null}
|
|
|
- </li>
|
|
|
- )
|
|
|
- })}
|
|
|
- </ul>
|
|
|
- </div>
|
|
|
+ {itemToSubtitle ? (
|
|
|
+ <span className="text-muted select-item-subtitle">
|
|
|
+ {itemToSubtitle(item)}
|
|
|
+ </span>
|
|
|
+ ) : null}
|
|
|
+ </li>
|
|
|
+ )
|
|
|
+ })}
|
|
|
+ </ul>
|
|
|
+ </div>
|
|
|
+ }
|
|
|
+ bs5={
|
|
|
+ <div className="select-wrapper" ref={rootRef}>
|
|
|
+ {label ? (
|
|
|
+ <Form.Label {...getLabelProps()}>
|
|
|
+ {label}{' '}
|
|
|
+ {optionalLabel && (
|
|
|
+ <span className="fw-normal">({t('optional')})</span>
|
|
|
+ )}{' '}
|
|
|
+ {loading && (
|
|
|
+ <span data-testid="spinner">
|
|
|
+ <Spinner
|
|
|
+ animation="border"
|
|
|
+ aria-hidden="true"
|
|
|
+ as="span"
|
|
|
+ role="status"
|
|
|
+ size="sm"
|
|
|
+ />
|
|
|
+ </span>
|
|
|
+ )}
|
|
|
+ </Form.Label>
|
|
|
+ ) : null}
|
|
|
+ <FormControl
|
|
|
+ {...getToggleButtonProps({
|
|
|
+ disabled,
|
|
|
+ onKeyDown,
|
|
|
+ className: 'select-trigger',
|
|
|
+ })}
|
|
|
+ value={value}
|
|
|
+ readOnly
|
|
|
+ append={
|
|
|
+ <MaterialIcon
|
|
|
+ type={isOpen ? 'keyboard_arrow_up' : 'keyboard_arrow_down'}
|
|
|
+ className="align-text-bottom"
|
|
|
+ />
|
|
|
+ }
|
|
|
+ />
|
|
|
+ <ul
|
|
|
+ {...getMenuProps({ disabled, onKeyDown: handleMenuKeyDown })}
|
|
|
+ className={classNames('dropdown-menu w-100', { show: isOpen })}
|
|
|
+ >
|
|
|
+ {isOpen &&
|
|
|
+ items?.map((item, index) => {
|
|
|
+ const isDisabled = itemToDisabled && itemToDisabled(item)
|
|
|
+ return (
|
|
|
+ <li role="none" key={itemToKey(item)}>
|
|
|
+ <DropdownItem
|
|
|
+ as="button"
|
|
|
+ className={classNames({
|
|
|
+ 'select-highlighted': highlightedIndex === index,
|
|
|
+ })}
|
|
|
+ active={selectedItem === item}
|
|
|
+ trailingIcon={selectedItem === item ? 'check' : undefined}
|
|
|
+ description={
|
|
|
+ itemToSubtitle ? itemToSubtitle(item) : undefined
|
|
|
+ }
|
|
|
+ {...getItemProps({ item, index, disabled: isDisabled })}
|
|
|
+ >
|
|
|
+ {itemToString(item)}
|
|
|
+ </DropdownItem>
|
|
|
+ </li>
|
|
|
+ )
|
|
|
+ })}
|
|
|
+ </ul>
|
|
|
+ </div>
|
|
|
+ }
|
|
|
+ />
|
|
|
)
|
|
|
}
|