@codinglane/dropdown
Version:
An easy-to-use react dropdown
141 lines (140 loc) • 7.65 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import React from 'react';
import * as Icons from 'react-feather';
import './Dropdown.css';
import * as Components from '../components';
import * as Contracts from '../contracts';
/**
* @param id
* The current id of the dropdown.
* @param value
* The current chosen value for the dropdown, typeof string.
* The value is not getting managed inside the component. You have to provide a function to manage the current chosen value.
* @param searchable
* Set this value to true, if you want to have a dropdown with an input field to search for a specific option
* @param className
* Set the classname of the dropdown, if you want to set your custom style.
* @param closeOnSelect
* Set this value to true, if you want that the options of the dropdown are getting closed as soon as you choose an option.
* @param fields
* These are the possible options for the dropdown. If you want to have grouped dropdown options, set the type of your fields to GroupedDropdownOptions
* and set the group tag for all of the fields.
* @param placeholder
* The placeholder for the dropdown. When the placeholder is set and the value is undefined or not assignable to a field, the placeholder is getting showed.
* @param onChange
* This is the function to manage the current chosen value for the dropdown.
* @param onFocus
* This function is getting called on focus of the options menu.
* @param onBlur
* This function is getting called on blur of the options menu.
* @param onFavorizeOption
* The on favorize option is getting called as soon as the favorize icon in the option menu is getting clicked.
* This dropdown do not manage the favorites on its own. You have to manage the favorites, to see changes in the component.
* @param favoriteLabels
* The labels for the favorite and non favorite group.
* @param style
* Custom stylesheet for the dropdown. It is possible to set the bg color, the color (for text & border), the font size and the font family.
* @param data-testid
* For testing purpose.
* @returns {JSX.Element}
*/
export const Dropdown = ({ id, fields, value, searchable = false, className, closeOnSelect = false, placeholder, onChange, onBlur, onFocus, onFavorizeOption, style, ...props }) => {
const dropdown = React.useRef();
const setDropdown = (ref) => (dropdown.current = ref);
const input = React.useRef();
const setInput = (ref) => (input.current = ref);
const menu = React.useRef();
const setMenu = (ref) => (menu.current = ref);
const textContainer = React.useRef();
const setTextContainer = (ref) => (textContainer.current = ref);
const text = React.useRef();
const setText = (ref) => (text.current = ref);
const [currentVisibleOptions, setCurrentVisibleOptions] = React.useState([]);
const grouped = fields.some((fld) => fld.group !== undefined);
const favorize = fields.some((fld) => fld.favorite !== undefined);
const [active, setActive] = React.useState(false);
const [search, setSearch] = React.useState(null);
const field = React.useMemo(() => fields.find((fld) => fld.value === value)?.label, [value, fields]);
const anchor = React.useMemo(() => {
if (!active)
return;
if (!input.current)
return;
const height = document.body.clientHeight;
const isScrollable = document.body.clientHeight < document.body.scrollHeight;
const at = input.current.getBoundingClientRect().bottom + 5;
if (input.current.getBoundingClientRect().bottom + Contracts.MENU_MAX_HEIGHT + 10 > height)
return {
at: height -
input.current.getBoundingClientRect().top +
5 -
(isScrollable ? Contracts.getScrollbarWidth() : 0),
direction: 'UP',
};
return { at, direction: 'DOWN' };
}, [active, input.current]);
const handleOptionClick = (option) => {
onChange(option);
setSearch(null);
if (!closeOnSelect)
return;
setActive(false);
};
React.useEffect(() => {
Contracts.setStyleSheet(style);
}, [style]);
React.useEffect(() => {
if (active && onFocus)
onFocus();
if (!active && onBlur)
onBlur();
}, [active]);
const handleSearch = (event) => setSearch(event.currentTarget.value);
const toggle = () => setActive((prev) => !prev);
const enterSearch = (event) => {
event.persist();
if (event.key.toLowerCase() !== 'enter')
return;
if (search === null)
return;
if (currentVisibleOptions.length > 0)
onChange(currentVisibleOptions[0]);
setSearch(null);
toggle();
};
React.useEffect(() => {
if (!input.current)
return;
if (active)
input.current.focus();
else {
setSearch(null);
input.current.blur();
}
}, [input.current, active]);
React.useEffect(() => {
const clickListener = (event) => {
if (dropdown.current?.contains(event.target))
return;
setActive(false);
};
const scrollListener = (event) => {
if (menu.current?.contains(event.target))
return;
setActive(false);
};
document.addEventListener('click', clickListener);
document.addEventListener('scroll', scrollListener, true);
return () => {
document.removeEventListener('click', clickListener);
document.removeEventListener('scroll', scrollListener, true);
};
}, []);
React.useLayoutEffect(() => {
if (!textContainer.current || !text.current || !input.current)
return;
textContainer.current.style.width = `${text.current.clientWidth + 17.5}px`;
input.current.style.width = `${text.current.clientWidth - 4.5}px`;
}, [text.current, textContainer.current, value]);
return (_jsxs("div", { className: `dropdown ${active ? 'active' : ''} ${className ?? ''}`, ref: setDropdown, ...props, children: [_jsxs("div", { className: 'customBase dropdown-search', children: [_jsxs("div", { className: 'dropdown-text'.concat(!searchable ? ' dropdown-readonly' : ''), onClick: toggle, ref: setTextContainer, children: [!search && (_jsx("div", { className: `dropdown-searchtext ${field === undefined ? 'dropdown-placeholder' : ''}`, ref: setText, children: field ?? placeholder })), _jsx(Components.BaseInput, { value: search ?? '', id: id?.concat('input'), onChange: handleSearch, className: 'dropdown-searchinput', autoComplete: 'off', autoCapitalize: 'off', autoCorrect: 'off', disabled: !searchable, ref: setInput, onKeyUp: enterSearch, "data-testid": props['data-testid']?.concat('-input') })] }), _jsx(Icons.ChevronDown, { size: 16, className: 'dropdown-searchicon', onClick: toggle })] }), grouped || favorize ? (_jsx(Components.Grouped, { id: id, onOptionClick: handleOptionClick, onFilteredChange: setCurrentVisibleOptions, options: fields, current: value, anchor: anchor, ref: setMenu, filter: search, onFavorize: onFavorizeOption, grouping: grouped, favorize: favorize, ...props })) : (_jsx(Components.Standard, { id: id, onOptionClick: handleOptionClick, options: fields, current: value, filter: search, anchor: anchor, ref: setMenu, onFilteredChange: setCurrentVisibleOptions, ...props }))] }));
};