mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-23 17:08:34 +00:00
Merge pull request #12550 from ToolJet/fix/dropdown-interactions
Fix: Changed Interaction and added option to remove the clear button and remove the search box
This commit is contained in:
commit
c068ba8208
13 changed files with 341 additions and 154 deletions
|
|
@ -75,6 +75,18 @@ export const dropdownV2Config = {
|
|||
accordian: 'Options',
|
||||
isFxNotRequired: true,
|
||||
},
|
||||
showClearBtn: {
|
||||
type: 'toggle',
|
||||
displayName: 'Show clear selection button',
|
||||
validation: { schema: { type: 'boolean' }, defaultValue: true },
|
||||
section: 'additionalActions',
|
||||
},
|
||||
showSearchInput: {
|
||||
type: 'toggle',
|
||||
displayName: 'Show search in options',
|
||||
validation: { schema: { type: 'boolean' }, defaultValue: true },
|
||||
section: 'additionalActions',
|
||||
},
|
||||
loadingState: {
|
||||
type: 'toggle',
|
||||
displayName: 'Loading state',
|
||||
|
|
@ -314,6 +326,8 @@ export const dropdownV2Config = {
|
|||
optionsLoadingState: { value: '{{false}}' },
|
||||
sort: { value: 'asc' },
|
||||
placeholder: { value: 'Select an option' },
|
||||
showClearBtn: { value: '{{true}}' },
|
||||
showSearchInput: { value: '{{true}}' },
|
||||
visibility: { value: '{{true}}' },
|
||||
disabledState: { value: '{{false}}' },
|
||||
loadingState: { value: '{{false}}' },
|
||||
|
|
|
|||
|
|
@ -142,6 +142,18 @@ export const multiselectV2Config = {
|
|||
accordian: 'Options',
|
||||
isFxNotRequired: true,
|
||||
},
|
||||
showClearBtn: {
|
||||
type: 'toggle',
|
||||
displayName: 'Show clear selection button',
|
||||
validation: { schema: { type: 'boolean' }, defaultValue: true },
|
||||
section: 'additionalActions',
|
||||
},
|
||||
showSearchInput: {
|
||||
type: 'toggle',
|
||||
displayName: 'Show search in options',
|
||||
validation: { schema: { type: 'boolean' }, defaultValue: true },
|
||||
section: 'additionalActions',
|
||||
},
|
||||
loadingState: {
|
||||
type: 'toggle',
|
||||
displayName: 'Loading state',
|
||||
|
|
@ -327,6 +339,8 @@ export const multiselectV2Config = {
|
|||
optionsLoadingState: { value: '{{false}}' },
|
||||
sort: { value: 'asc' },
|
||||
placeholder: { value: 'Select the options' },
|
||||
showClearBtn: { value: '{{true}}' },
|
||||
showSearchInput: { value: '{{true}}' },
|
||||
visibility: { value: '{{true}}' },
|
||||
disabledState: { value: '{{false}}' },
|
||||
loadingState: { value: '{{false}}' },
|
||||
|
|
|
|||
|
|
@ -1,87 +1,118 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { components } from 'react-select';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import Loader from '@/ToolJetUI/Loader/Loader';
|
||||
import './dropdownV2.scss';
|
||||
import { FormCheck } from 'react-bootstrap';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import cx from 'classnames';
|
||||
|
||||
const { MenuList } = components;
|
||||
|
||||
// This Menulist also used in MultiselectV2
|
||||
const CustomMenuList = ({ selectProps, ...props }) => {
|
||||
const {
|
||||
onInputChange,
|
||||
onMenuInputFocus,
|
||||
showAllOption,
|
||||
isSelectAllSelected,
|
||||
optionsLoadingState,
|
||||
darkMode,
|
||||
setSelected,
|
||||
setIsSelectAllSelected,
|
||||
fireEvent,
|
||||
inputValue,
|
||||
menuId,
|
||||
} = selectProps;
|
||||
const { onInputChange, onMenuInputFocus, optionsLoadingState, darkMode, inputValue, menuId, showSearchInput } =
|
||||
selectProps;
|
||||
|
||||
const handleSelectAll = (e) => {
|
||||
e.target.checked && fireEvent();
|
||||
if (e.target.checked) {
|
||||
setSelected(props.options);
|
||||
} else {
|
||||
setSelected([]);
|
||||
const parentRef = useRef(null);
|
||||
const virtualizer = useVirtualizer({
|
||||
count: props?.children?.length || 0,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 40,
|
||||
overscan: 15,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const searchInput = document.querySelector('.dropdown-multiselect-widget-search-box');
|
||||
if (searchInput) {
|
||||
searchInput.focus();
|
||||
}
|
||||
setIsSelectAllSelected(e.target.checked);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
id={`dropdown-multiselect-widget-custom-menu-list-${menuId}`}
|
||||
className={cx('dropdown-multiselect-widget-custom-menu-list', { 'theme-dark dark-theme': darkMode })}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onTouchEnd={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="dropdown-multiselect-widget-search-box-wrapper">
|
||||
<span>
|
||||
<SolidIcon name="search01" width="14" />
|
||||
</span>
|
||||
<input
|
||||
autoCorrect="off"
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={(e) =>
|
||||
onInputChange(e.currentTarget.value, {
|
||||
action: 'input-change',
|
||||
})
|
||||
}
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
e.target.focus();
|
||||
}}
|
||||
onTouchEnd={(e) => {
|
||||
e.stopPropagation();
|
||||
e.target.focus();
|
||||
}}
|
||||
onFocus={onMenuInputFocus}
|
||||
placeholder="Search"
|
||||
className="dropdown-multiselect-widget-search-box"
|
||||
/>
|
||||
</div>
|
||||
{showAllOption && !optionsLoadingState && (
|
||||
<label htmlFor="select-all-checkbox" className="multiselect-custom-menulist-select-all">
|
||||
<FormCheck id="select-all-checkbox" checked={isSelectAllSelected} onChange={handleSelectAll} />
|
||||
<span style={{ marginLeft: '4px' }}>Select all</span>
|
||||
</label>
|
||||
{showSearchInput && (
|
||||
<div className="dropdown-multiselect-widget-search-box-wrapper">
|
||||
<span>
|
||||
<SolidIcon name="search01" width="14" />
|
||||
</span>
|
||||
<input
|
||||
autoCorrect="off"
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={(e) =>
|
||||
onInputChange(e.currentTarget.value, {
|
||||
action: 'input-change',
|
||||
})
|
||||
}
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
e.target.focus();
|
||||
}}
|
||||
onTouchEnd={(e) => {
|
||||
e.stopPropagation();
|
||||
e.target.focus();
|
||||
}}
|
||||
onFocus={onMenuInputFocus}
|
||||
placeholder="Search"
|
||||
className="dropdown-multiselect-widget-search-box"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<MenuList {...props} selectProps={selectProps}>
|
||||
{optionsLoadingState ? (
|
||||
<div class="text-center py-4" style={{ minHeight: '188px' }}>
|
||||
<Loader style={{ zIndex: 3, position: 'absolute' }} width="36" />
|
||||
{!optionsLoadingState && (
|
||||
<div
|
||||
ref={parentRef}
|
||||
className="dropdown-multiselect-widget-custom-menu-list-body"
|
||||
style={{
|
||||
maxHeight: selectProps.maxMenuHeight || 300,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: `${virtualizer.getTotalSize() || 38}px`,
|
||||
position: 'relative',
|
||||
marginTop: '5px',
|
||||
}}
|
||||
>
|
||||
{!virtualizer.getTotalSize() && props.children}
|
||||
{virtualizer.getVirtualItems().map((virtualItem) => {
|
||||
const option = props.options[virtualItem.index];
|
||||
const child = props.children[virtualItem.index];
|
||||
const isSelectAll = option?.value === 'multiselect-custom-menulist-select-all';
|
||||
return (
|
||||
<div
|
||||
key={option.value}
|
||||
style={{
|
||||
position: isSelectAll ? 'sticky' : 'absolute',
|
||||
width: '100%',
|
||||
top: 0,
|
||||
zIndex: isSelectAll && 10,
|
||||
transform: `translateY(${virtualItem.start}px)`,
|
||||
}}
|
||||
data-index={virtualItem.index}
|
||||
ref={virtualizer.measureElement}
|
||||
>
|
||||
<MenuList {...props} selectProps={selectProps}>
|
||||
<div>{child}</div>
|
||||
</MenuList>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
props.children
|
||||
)}
|
||||
</MenuList>
|
||||
</div>
|
||||
)}
|
||||
{optionsLoadingState && (
|
||||
<div className="text-center py-4" style={{ minHeight: '188px' }}>
|
||||
<Loader style={{ zIndex: 3, position: 'absolute' }} width="36" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,7 +6,17 @@ import { highlightText } from './utils';
|
|||
|
||||
const CustomOption = (props) => {
|
||||
return (
|
||||
<components.Option {...props}>
|
||||
<components.Option
|
||||
{...props}
|
||||
innerProps={{
|
||||
...props.innerProps,
|
||||
onTouchEnd: (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
props.selectOption(props.data);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className="cursor-pointer">
|
||||
{props.isSelected && (
|
||||
<span style={{ maxHeight: '20px', marginRight: '8px', marginLeft: '-28px' }}>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import Label from '@/_ui/Label';
|
|||
import cx from 'classnames';
|
||||
import { getInputBackgroundColor, getInputBorderColor, getInputFocusedColor, sortArray } from './utils';
|
||||
import { isMobileDevice } from '@/_helpers/appUtils';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
|
||||
const { DropdownIndicator, ClearIndicator } = components;
|
||||
const INDICATOR_CONTAINER_WIDTH = 60;
|
||||
|
|
@ -69,6 +68,8 @@ export const DropdownV2 = ({
|
|||
disabledState,
|
||||
optionsLoadingState,
|
||||
sort,
|
||||
showClearBtn,
|
||||
showSearchInput,
|
||||
} = properties;
|
||||
const {
|
||||
selectedTextColor,
|
||||
|
|
@ -104,8 +105,6 @@ export const DropdownV2 = ({
|
|||
const [isDropdownDisabled, setIsDropdownDisabled] = useState(disabledState);
|
||||
const [searchInputValue, setSearchInputValue] = useState('');
|
||||
const [userInteracted, setUserInteracted] = useState(false);
|
||||
const currentMode = useStore((state) => state.currentMode);
|
||||
const isEditor = currentMode === 'edit';
|
||||
|
||||
const _height = padding === 'default' ? `${height}px` : `${height + 4}px`;
|
||||
const labelRef = useRef();
|
||||
|
|
@ -173,12 +172,43 @@ export const DropdownV2 = ({
|
|||
setExposedVariable('isValid', validationStatus?.isValid);
|
||||
};
|
||||
|
||||
const handleClickInEditor = (e) => {
|
||||
if (e.target.className.includes('clear-indicator') || isMenuOpen) return;
|
||||
e.stopPropagation();
|
||||
selectRef.current?.onControlMouseDown(e);
|
||||
const handleClickInsideSelect = () => {
|
||||
if (isDropdownDisabled || isDropdownLoading) return;
|
||||
if (isMenuOpen) {
|
||||
setIsMenuOpen(false);
|
||||
fireEvent('onBlur');
|
||||
setSearchInputValue('');
|
||||
} else {
|
||||
setIsMenuOpen(true);
|
||||
fireEvent('onFocus');
|
||||
if (!showSearchInput) {
|
||||
selectRef.current.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickOutsideSelect = (event) => {
|
||||
const menu = document.querySelector(`._tooljet-${componentName}`);
|
||||
if (
|
||||
isMenuOpen &&
|
||||
menu &&
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target) &&
|
||||
!menu.contains(event.target)
|
||||
) {
|
||||
setIsMenuOpen(false);
|
||||
fireEvent('onBlur');
|
||||
setSearchInputValue('');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClickOutsideSelect);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutsideSelect);
|
||||
};
|
||||
}, [isMenuOpen, componentName]);
|
||||
|
||||
useEffect(() => {
|
||||
setInputValue(findDefaultItem(advanced ? schema : options));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
|
@ -386,7 +416,7 @@ export const DropdownV2 = ({
|
|||
}),
|
||||
menuList: (provided) => ({
|
||||
...provided,
|
||||
padding: '8px',
|
||||
padding: '0 8px',
|
||||
borderRadius: '8px',
|
||||
// this is needed otherwise :active state doesn't look nice, gap is required
|
||||
display: 'flex',
|
||||
|
|
@ -446,7 +476,8 @@ export const DropdownV2 = ({
|
|||
<div
|
||||
className="w-100 px-0 h-100 dropdownV2-widget"
|
||||
ref={ref}
|
||||
onMouseDownCapture={isEditor && handleClickInEditor}
|
||||
onClick={handleClickInsideSelect}
|
||||
onTouchEnd={handleClickInsideSelect}
|
||||
>
|
||||
<Select
|
||||
ref={selectRef}
|
||||
|
|
@ -456,17 +487,21 @@ export const DropdownV2 = ({
|
|||
onChange={(selectedOption, actionProps) => {
|
||||
if (actionProps.action === 'clear') {
|
||||
setInputValue(null);
|
||||
fireEvent('onSelect');
|
||||
}
|
||||
if (actionProps.action === 'select-option') {
|
||||
setInputValue(selectedOption.value);
|
||||
fireEvent('onSelect');
|
||||
if (currentValue === selectedOption.value) {
|
||||
setInputValue(null);
|
||||
} else setInputValue(selectedOption.value);
|
||||
}
|
||||
fireEvent('onSelect');
|
||||
setSearchInputValue('');
|
||||
setIsMenuOpen(false);
|
||||
setUserInteracted(true);
|
||||
}}
|
||||
options={selectOptions}
|
||||
styles={customStyles}
|
||||
isLoading={isDropdownLoading}
|
||||
showSearchInput={showSearchInput}
|
||||
onInputChange={onSearchTextChange}
|
||||
inputValue={searchInputValue}
|
||||
placeholder={placeholder}
|
||||
|
|
@ -477,7 +512,7 @@ export const DropdownV2 = ({
|
|||
Option: CustomOption,
|
||||
LoadingIndicator: () => <Loader style={{ right: '11px', zIndex: 3, position: 'absolute' }} width="16" />,
|
||||
DropdownIndicator: isDropdownLoading ? () => null : CustomDropdownIndicator,
|
||||
ClearIndicator: CustomClearIndicator,
|
||||
ClearIndicator: showClearBtn ? CustomClearIndicator : () => null,
|
||||
}}
|
||||
isClearable
|
||||
tabSelectsValue={false}
|
||||
|
|
@ -488,21 +523,15 @@ export const DropdownV2 = ({
|
|||
darkMode={darkMode}
|
||||
optionsLoadingState={optionsLoadingState && advanced}
|
||||
menuPlacement="auto"
|
||||
onMenuOpen={() => {
|
||||
setIsMenuOpen(true);
|
||||
fireEvent('onFocus');
|
||||
}}
|
||||
onMenuClose={() => {
|
||||
setIsMenuOpen(false);
|
||||
fireEvent('onBlur');
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !isMenuOpen) {
|
||||
if (e.key === 'Enter' && !isMenuOpen && !isDropdownLoading) {
|
||||
setIsMenuOpen(true);
|
||||
fireEvent('onFocus');
|
||||
e.preventDefault();
|
||||
}
|
||||
if (e.key === 'Escape' && isMenuOpen) {
|
||||
setIsMenuOpen(false);
|
||||
fireEvent('onBlur');
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
.dropdown-multiselect-widget-custom-menu-list-body {
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
padding: 0 0 5px;
|
||||
background-color: var(--surfaces-surface-01) !important;
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
|
|
@ -7,11 +7,23 @@ import { highlightText } from '../DropdownV2/utils';
|
|||
|
||||
const CustomOption = (props) => {
|
||||
return (
|
||||
<Option {...props}>
|
||||
<Option
|
||||
{...props}
|
||||
innerProps={{
|
||||
...props.innerProps,
|
||||
onTouchEnd: (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
props.selectOption(props.data);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className="d-flex multiselct-widget-option">
|
||||
<FormCheck checked={props.isSelected} disabled={props?.isDisabled} />
|
||||
<span style={{ marginLeft: '5px' }}>
|
||||
{highlightText(props.label?.toString(), props.selectProps.inputValue)}
|
||||
{props.label?.includes('Select all')
|
||||
? 'Select all'
|
||||
: highlightText(props.label?.toString(), props.selectProps.inputValue)}
|
||||
</span>
|
||||
</div>
|
||||
</Option>
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ const CustomValueContainer = ({ children, ...props }) => {
|
|||
{/* Rendering children except Placeholder component to preserve the default behavior of react-select like focus
|
||||
handling */}
|
||||
{React.Children.map(children, (child) => {
|
||||
if (child.type !== Placeholder) {
|
||||
if (child?.type !== Placeholder) {
|
||||
return child;
|
||||
}
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import Label from '@/_ui/Label';
|
|||
const tinycolor = require('tinycolor2');
|
||||
import { CustomDropdownIndicator, CustomClearIndicator } from '../DropdownV2/DropdownV2';
|
||||
import { getInputBackgroundColor, getInputBorderColor, getInputFocusedColor, sortArray } from '../DropdownV2/utils';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
|
||||
export const MultiselectV2 = ({
|
||||
id,
|
||||
|
|
@ -38,6 +37,8 @@ export const MultiselectV2 = ({
|
|||
loadingState: multiSelectLoadingState,
|
||||
optionsLoadingState,
|
||||
sort,
|
||||
showClearBtn,
|
||||
showSearchInput,
|
||||
} = properties;
|
||||
const {
|
||||
selectedTextColor,
|
||||
|
|
@ -73,12 +74,9 @@ export const MultiselectV2 = ({
|
|||
const [visibility, setVisibility] = useState(properties.visibility);
|
||||
const [isMultiSelectLoading, setIsMultiSelectLoading] = useState(multiSelectLoadingState);
|
||||
const [isMultiSelectDisabled, setIsMultiSelectDisabled] = useState(disabledState);
|
||||
const [isSelectAllSelected, setIsSelectAllSelected] = useState(false);
|
||||
const [searchInputValue, setSearchInputValue] = useState('');
|
||||
const _height = padding === 'default' ? `${height}px` : `${height + 4}px`;
|
||||
const [userInteracted, setUserInteracted] = useState(false);
|
||||
const currentMode = useStore((state) => state.currentMode);
|
||||
const isEditor = currentMode === 'edit';
|
||||
|
||||
const [isMultiselectOpen, setIsMultiselectOpen] = useState(false);
|
||||
useEffect(() => {
|
||||
|
|
@ -93,18 +91,29 @@ export const MultiselectV2 = ({
|
|||
const _options = advanced ? schema : options;
|
||||
let _selectOptions = Array.isArray(_options)
|
||||
? _options
|
||||
.filter((data) => data?.visible ?? true)
|
||||
.map((data) => ({
|
||||
...data,
|
||||
label: data?.label,
|
||||
value: data?.value,
|
||||
isDisabled: data?.disable ?? false,
|
||||
}))
|
||||
.filter((data) => data?.visible ?? true)
|
||||
.map((data) => ({
|
||||
...data,
|
||||
label: data?.label,
|
||||
value: data?.value,
|
||||
isDisabled: data?.disable ?? false,
|
||||
}))
|
||||
: [];
|
||||
return sortArray(_selectOptions, sort);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [advanced, JSON.stringify(schema), JSON.stringify(options), sort]);
|
||||
|
||||
const modifiedSelectOptions = useMemo(() => {
|
||||
// Adding select all option dynamically to the options
|
||||
if (showAllOption && !optionsLoadingState) {
|
||||
return [
|
||||
// Appended search input value so that it is always visible
|
||||
{ label: `Select all ${searchInputValue}`, value: 'multiselect-custom-menulist-select-all' },
|
||||
...selectOptions,
|
||||
];
|
||||
} else return selectOptions;
|
||||
}, [showAllOption, JSON.stringify(selectOptions), optionsLoadingState, searchInputValue]);
|
||||
|
||||
function findDefaultItem(value, isAdvanced, isDefault) {
|
||||
if (isAdvanced) {
|
||||
const foundItem = Array.isArray(schema) ? schema.filter((item) => item?.visible && item?.default) : [];
|
||||
|
|
@ -129,8 +138,28 @@ export const MultiselectV2 = ({
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const onChangeHandler = (items, action) => {
|
||||
setInputValue(items);
|
||||
const SELECT_ALL = 'multiselect-custom-menulist-select-all';
|
||||
|
||||
if (action.option?.value === SELECT_ALL) {
|
||||
// Case 1 - If select all is selected
|
||||
if (action.action === 'select-option') {
|
||||
setInputValue(modifiedSelectOptions);
|
||||
} else {
|
||||
setInputValue([]);
|
||||
}
|
||||
} else if (items?.some((item) => item.value === SELECT_ALL)) {
|
||||
// Case 2 - If select all is not selected but selected options include select all
|
||||
setInputValue(items.filter((item) => item.value !== SELECT_ALL));
|
||||
} else if (selectOptions?.length === items?.length) {
|
||||
// Case 3 - If all options are selected except select all
|
||||
setInputValue(modifiedSelectOptions);
|
||||
} else {
|
||||
// Case 4 - Normal selection
|
||||
setInputValue(items);
|
||||
}
|
||||
|
||||
fireEvent('onSelect');
|
||||
setUserInteracted(true);
|
||||
};
|
||||
|
|
@ -270,26 +299,34 @@ export const MultiselectV2 = ({
|
|||
fireEvent('onSearchTextChanged');
|
||||
}
|
||||
};
|
||||
const handleClickOutside = (event) => {
|
||||
const handleClickOutsideSelect = (event) => {
|
||||
let menu = document.getElementById(`dropdown-multiselect-widget-custom-menu-list-${id}`);
|
||||
if (
|
||||
isMultiselectOpen &&
|
||||
multiselectRef.current &&
|
||||
!multiselectRef.current.contains(event.target) &&
|
||||
menu &&
|
||||
!menu.contains(event.target)
|
||||
) {
|
||||
if (isMultiselectOpen) {
|
||||
fireEvent('onBlur');
|
||||
setIsMultiselectOpen(false);
|
||||
setSearchInputValue('');
|
||||
}
|
||||
setIsMultiselectOpen(false);
|
||||
fireEvent('onBlur');
|
||||
setSearchInputValue('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickInEditor = (e) => {
|
||||
if (e.target.className.includes('clear-indicator') || isMultiselectOpen) return;
|
||||
e.stopPropagation();
|
||||
selectRef.current?.onControlMouseDown(e);
|
||||
const handleClickInsideSelect = () => {
|
||||
if (isMultiSelectDisabled || isMultiSelectLoading) return;
|
||||
if (isMultiselectOpen) {
|
||||
setIsMultiselectOpen(false);
|
||||
fireEvent('onBlur');
|
||||
setSearchInputValue('');
|
||||
} else {
|
||||
setIsMultiselectOpen(true);
|
||||
fireEvent('onFocus');
|
||||
if (!showSearchInput) {
|
||||
selectRef.current.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const setInputValue = (values) => {
|
||||
|
|
@ -304,20 +341,11 @@ export const MultiselectV2 = ({
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside, { capture: true });
|
||||
document.addEventListener('mousedown', handleClickOutsideSelect, { capture: true });
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside, { capture: true });
|
||||
document.removeEventListener('mousedown', handleClickOutsideSelect, { capture: true });
|
||||
};
|
||||
}, [isMultiselectOpen]);
|
||||
|
||||
// Handle Select all logic
|
||||
useEffect(() => {
|
||||
if (selectOptions?.length === selected?.length) {
|
||||
setIsSelectAllSelected(true);
|
||||
} else {
|
||||
setIsSelectAllSelected(false);
|
||||
}
|
||||
}, [selectOptions, selected]);
|
||||
}, [isMultiselectOpen, componentName]);
|
||||
|
||||
const customStyles = {
|
||||
container: (base) => ({
|
||||
|
|
@ -365,8 +393,8 @@ export const MultiselectV2 = ({
|
|||
selectedTextColor !== '#1B1F24'
|
||||
? selectedTextColor
|
||||
: isMultiSelectLoading || isMultiSelectDisabled
|
||||
? 'var(--text-disabled)'
|
||||
: 'var(--text-primary)',
|
||||
? 'var(--text-disabled)'
|
||||
: 'var(--text-primary)',
|
||||
}),
|
||||
|
||||
input: (provided, _state) => ({
|
||||
|
|
@ -401,10 +429,10 @@ export const MultiselectV2 = ({
|
|||
color: _state.isDisabled
|
||||
? 'var(_--text-disbled)'
|
||||
: selectedTextColor !== '#1B1F24'
|
||||
? selectedTextColor
|
||||
: isMultiSelectDisabled || isMultiSelectLoading
|
||||
? 'var(--text-disabled)'
|
||||
: 'var(--text-primary)',
|
||||
? selectedTextColor
|
||||
: isMultiSelectDisabled || isMultiSelectLoading
|
||||
? 'var(--text-disabled)'
|
||||
: 'var(--text-primary)',
|
||||
borderRadius: _state.isFocused && '8px',
|
||||
padding: '8px 6px 8px 12px',
|
||||
'&:hover': {
|
||||
|
|
@ -415,7 +443,7 @@ export const MultiselectV2 = ({
|
|||
}),
|
||||
menuList: (provided) => ({
|
||||
...provided,
|
||||
padding: '4px',
|
||||
padding: '0 4px',
|
||||
// this is needed otherwise :active state doesn't look nice, gap is required
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
|
@ -426,6 +454,7 @@ export const MultiselectV2 = ({
|
|||
menu: (provided) => ({
|
||||
...provided,
|
||||
marginTop: '5px',
|
||||
borderRadius: '8px',
|
||||
}),
|
||||
};
|
||||
const _width = (labelWidth / 100) * 70; // Max width which label can go is 70% for better UX calculate width based on this value
|
||||
|
|
@ -436,7 +465,7 @@ export const MultiselectV2 = ({
|
|||
data-cy={`label-${String(componentName).toLowerCase()} `}
|
||||
className={cx('multiselect-widget', 'd-flex', {
|
||||
[alignment === 'top' &&
|
||||
((labelWidth != 0 && label?.length != 0) || (auto && labelWidth == 0 && label && label?.length != 0))
|
||||
((labelWidth != 0 && label?.length != 0) || (auto && labelWidth == 0 && label && label?.length != 0))
|
||||
? 'flex-column'
|
||||
: 'align-items-center']: true,
|
||||
'flex-row-reverse': direction === 'right' && alignment === 'side',
|
||||
|
|
@ -468,17 +497,18 @@ export const MultiselectV2 = ({
|
|||
_width={_width}
|
||||
top={'1px'}
|
||||
/>
|
||||
<div className="w-100 px-0 h-100" onMouseDownCapture={isEditor && handleClickInEditor}>
|
||||
<div className="w-100 px-0 h-100" onClick={handleClickInsideSelect} onTouchEnd={handleClickInsideSelect}>
|
||||
<Select
|
||||
ref={selectRef}
|
||||
menuId={id}
|
||||
isDisabled={isMultiSelectDisabled}
|
||||
value={selected}
|
||||
onChange={onChangeHandler}
|
||||
options={selectOptions}
|
||||
options={modifiedSelectOptions}
|
||||
styles={customStyles}
|
||||
// Only show loading when dynamic options are enabled
|
||||
isLoading={isMultiSelectLoading}
|
||||
showSearchInput={showSearchInput}
|
||||
onInputChange={onSearchTextChange}
|
||||
inputValue={searchInputValue}
|
||||
menuIsOpen={isMultiselectOpen}
|
||||
|
|
@ -488,7 +518,7 @@ export const MultiselectV2 = ({
|
|||
ValueContainer: CustomValueContainer,
|
||||
Option: CustomOption,
|
||||
LoadingIndicator: () => <Loader style={{ right: '11px', zIndex: 3, position: 'absolute' }} width="16" />,
|
||||
ClearIndicator: CustomClearIndicator,
|
||||
ClearIndicator: showClearBtn ? CustomClearIndicator : () => null,
|
||||
DropdownIndicator: isMultiSelectLoading ? () => null : CustomDropdownIndicator,
|
||||
}}
|
||||
isClearable
|
||||
|
|
@ -498,21 +528,15 @@ export const MultiselectV2 = ({
|
|||
tabSelectsValue={false}
|
||||
controlShouldRenderValue={false}
|
||||
isSearchable={false}
|
||||
onMenuOpen={() => {
|
||||
setIsMultiselectOpen(true);
|
||||
fireEvent('onFocus');
|
||||
}}
|
||||
onMenuClose={() => {
|
||||
setIsMultiselectOpen(false);
|
||||
fireEvent('onBlur');
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !isMultiselectOpen) {
|
||||
if (e.key === 'Enter' && !isMultiselectOpen && !isMultiSelectLoading) {
|
||||
setIsMultiselectOpen(true);
|
||||
fireEvent('onFocus');
|
||||
e.preventDefault();
|
||||
}
|
||||
if (e.key === 'Escape' && isMultiselectOpen) {
|
||||
setIsMultiselectOpen(false);
|
||||
fireEvent('onBlur');
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
|
|
@ -520,19 +544,9 @@ export const MultiselectV2 = ({
|
|||
icon={icon}
|
||||
doShowIcon={iconVisibility}
|
||||
containerRef={valueContainerRef}
|
||||
showAllOption={showAllOption}
|
||||
isSelectAllSelected={isSelectAllSelected}
|
||||
setIsSelectAllSelected={(value) => {
|
||||
setIsSelectAllSelected(value);
|
||||
if (!value) {
|
||||
fireEvent('onSelect');
|
||||
}
|
||||
}}
|
||||
setSelected={setInputValue}
|
||||
iconColor={iconColor}
|
||||
optionsLoadingState={optionsLoadingState && advanced}
|
||||
darkMode={darkMode}
|
||||
fireEvent={() => fireEvent('onSelect')}
|
||||
menuPlacement="auto"
|
||||
menuPortalTarget={document.body}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -75,6 +75,18 @@ export const dropdownV2Config = {
|
|||
accordian: 'Options',
|
||||
isFxNotRequired: true,
|
||||
},
|
||||
showClearBtn: {
|
||||
type: 'toggle',
|
||||
displayName: 'Show clear selection button',
|
||||
validation: { schema: { type: 'boolean' }, defaultValue: true },
|
||||
section: 'additionalActions',
|
||||
},
|
||||
showSearchInput: {
|
||||
type: 'toggle',
|
||||
displayName: 'Show search in options',
|
||||
validation: { schema: { type: 'boolean' }, defaultValue: true },
|
||||
section: 'additionalActions',
|
||||
},
|
||||
loadingState: {
|
||||
type: 'toggle',
|
||||
displayName: 'Loading state',
|
||||
|
|
@ -314,6 +326,8 @@ export const dropdownV2Config = {
|
|||
optionsLoadingState: { value: '{{false}}' },
|
||||
sort: { value: 'asc' },
|
||||
placeholder: { value: 'Select an option' },
|
||||
showClearBtn: { value: '{{true}}' },
|
||||
showSearchInput: { value: '{{true}}' },
|
||||
visibility: { value: '{{true}}' },
|
||||
disabledState: { value: '{{false}}' },
|
||||
loadingState: { value: '{{false}}' },
|
||||
|
|
|
|||
|
|
@ -142,6 +142,18 @@ export const multiselectV2Config = {
|
|||
accordian: 'Options',
|
||||
isFxNotRequired: true,
|
||||
},
|
||||
showClearBtn: {
|
||||
type: 'toggle',
|
||||
displayName: 'Show clear selection button',
|
||||
validation: { schema: { type: 'boolean' }, defaultValue: true },
|
||||
section: 'additionalActions',
|
||||
},
|
||||
showSearchInput: {
|
||||
type: 'toggle',
|
||||
displayName: 'Show search in options',
|
||||
validation: { schema: { type: 'boolean' }, defaultValue: true },
|
||||
section: 'additionalActions',
|
||||
},
|
||||
loadingState: {
|
||||
type: 'toggle',
|
||||
displayName: 'Loading state',
|
||||
|
|
@ -327,6 +339,8 @@ export const multiselectV2Config = {
|
|||
optionsLoadingState: { value: '{{false}}' },
|
||||
sort: { value: 'asc' },
|
||||
placeholder: { value: 'Select the options' },
|
||||
showClearBtn: { value: '{{true}}' },
|
||||
showSearchInput: { value: '{{true}}' },
|
||||
visibility: { value: '{{true}}' },
|
||||
disabledState: { value: '{{false}}' },
|
||||
loadingState: { value: '{{false}}' },
|
||||
|
|
|
|||
|
|
@ -75,6 +75,18 @@ export const dropdownV2Config = {
|
|||
accordian: 'Options',
|
||||
isFxNotRequired: true,
|
||||
},
|
||||
showClearBtn: {
|
||||
type: 'toggle',
|
||||
displayName: 'Show clear selection button',
|
||||
validation: { schema: { type: 'boolean' }, defaultValue: true },
|
||||
section: 'additionalActions',
|
||||
},
|
||||
showSearchInput: {
|
||||
type: 'toggle',
|
||||
displayName: 'Show search in options',
|
||||
validation: { schema: { type: 'boolean' }, defaultValue: true },
|
||||
section: 'additionalActions',
|
||||
},
|
||||
loadingState: {
|
||||
type: 'toggle',
|
||||
displayName: 'Loading state',
|
||||
|
|
@ -314,6 +326,8 @@ export const dropdownV2Config = {
|
|||
optionsLoadingState: { value: '{{false}}' },
|
||||
sort: { value: 'asc' },
|
||||
placeholder: { value: 'Select an option' },
|
||||
showClearBtn: { value: '{{true}}' },
|
||||
showSearchInput: { value: '{{true}}' },
|
||||
visibility: { value: '{{true}}' },
|
||||
disabledState: { value: '{{false}}' },
|
||||
loadingState: { value: '{{false}}' },
|
||||
|
|
|
|||
|
|
@ -142,6 +142,18 @@ export const multiselectV2Config = {
|
|||
accordian: 'Options',
|
||||
isFxNotRequired: true,
|
||||
},
|
||||
showClearBtn: {
|
||||
type: 'toggle',
|
||||
displayName: 'Show clear selection button',
|
||||
validation: { schema: { type: 'boolean' }, defaultValue: true },
|
||||
section: 'additionalActions',
|
||||
},
|
||||
showSearchInput: {
|
||||
type: 'toggle',
|
||||
displayName: 'Show search in options',
|
||||
validation: { schema: { type: 'boolean' }, defaultValue: true },
|
||||
section: 'additionalActions',
|
||||
},
|
||||
loadingState: {
|
||||
type: 'toggle',
|
||||
displayName: 'Loading state',
|
||||
|
|
@ -327,6 +339,8 @@ export const multiselectV2Config = {
|
|||
optionsLoadingState: { value: '{{false}}' },
|
||||
sort: { value: 'asc' },
|
||||
placeholder: { value: 'Select the options' },
|
||||
showClearBtn: { value: '{{true}}' },
|
||||
showSearchInput: { value: '{{true}}' },
|
||||
visibility: { value: '{{true}}' },
|
||||
disabledState: { value: '{{false}}' },
|
||||
loadingState: { value: '{{false}}' },
|
||||
|
|
|
|||
Loading…
Reference in a new issue