diff --git a/packages/ui/src/core/autocomplete/autocomplete.component.tsx b/packages/ui/src/core/autocomplete/autocomplete.component.tsx index 0453092f..276f321d 100644 --- a/packages/ui/src/core/autocomplete/autocomplete.component.tsx +++ b/packages/ui/src/core/autocomplete/autocomplete.component.tsx @@ -4,17 +4,16 @@ import Tick from '@scaleflex/icons/tick'; import { intrinsicComponent } from '../../utils/functions'; import type { AutocompleteProps, AutocompleteOptionType, AutocompleteHookReturn } from './autocomplete.props'; -import ArrowTick from '../arrow-tick'; import Input from '../input'; import Tag from '../tag'; import MenuItem, { MenuItemActions } from '../menu-item'; import { InputSize } from '../../utils/types'; -import { Size } from '../menu-item/types'; import Styled from './autocomplete.styles'; import { renderLabel, renderHint, defaultGetOptionValue, defaultGetOptionLabel } from './autocomplete.utils'; import { useAutocomplete } from './autocomplete.hook'; import TextWithHighlights from '../text-with-highlights'; import EllipsedText from '../ellipsed-text'; +import Button from '../button/button.component'; const Autocomplete = intrinsicComponent( (props: AutocompleteProps, ref): JSX.Element => { @@ -49,6 +48,10 @@ const Autocomplete = intrinsicComponent( renderTag, renderMenuItem: renderMenuItemCustomFn, onChange, + selectAllButtonLabel = 'Select all', + clearAllButtonLabel = 'Clear all', + onClearAll, + onSelectAll, ...rest } = props; const { @@ -71,6 +74,7 @@ const Autocomplete = intrinsicComponent( handleKeyDown, handleClearIconClick, checkIsIdSelected, + handleSelectAllOptions, getOptionById, focusedMenuItemIndex, }: Partial = useAutocomplete({ @@ -82,6 +86,24 @@ const Autocomplete = intrinsicComponent( }); const isMultiple = Boolean(multiple) && Array.isArray(formattedValue); + const handleSelectAll = (event: React.MouseEvent): void => { + handleSelectAllOptions(); + event.stopPropagation(); + + if (onSelectAll) { + onSelectAll(event); + } + }; + + const handleClearAll = (event: React.MouseEvent): void => { + handleClearIconClick(); + event.stopPropagation(); + + if (onClearAll) { + onClearAll(event); + } + }; + const renderMenuItem = (option: AutocompleteOptionType, index: number): JSX.Element | React.ReactNode => { const optionId = getOptionValue(option); const optionLabel = getOptionLabel(option); @@ -191,6 +213,41 @@ const Autocomplete = intrinsicComponent( ); }; + const renderInputEndIcons = (): React.ReactNode => ( + <> + {renderLabelIconEnd && + renderLabelIconEnd({ + isMultiple, + option: + !isMultiple && formattedValue && typeof formattedValue === 'string' + ? getOptionById(formattedValue) + : null, + })} + + {!hideArrow && ( + + )} + + ); + + const renderInputActions = (): React.ReactNode => ( + + <> + + + + {renderInputEndIcons()} + + ); + return ( {renderLabel({ label, error, size, LabelProps: LabelPropsData })} @@ -212,26 +269,8 @@ const Autocomplete = intrinsicComponent( placeholder={placeholder} fullWidth={fullWidth} isEllipsis - iconEnd={() => ( - - {renderLabelIconEnd && - renderLabelIconEnd({ - isMultiple, - option: - !isMultiple && formattedValue && typeof formattedValue === 'string' - ? getOptionById(formattedValue) - : null, - })} - - {!hideArrow && ( - - )} - - )} + iconEnd={isMultiple ? undefined : renderInputEndIcons} + inputActions={isMultiple ? renderInputActions() : undefined} {...(showClearIcon ? { clearIcon: isValueSelected && , diff --git a/packages/ui/src/core/autocomplete/autocomplete.hook.ts b/packages/ui/src/core/autocomplete/autocomplete.hook.ts index 7ed72067..53f98eae 100644 --- a/packages/ui/src/core/autocomplete/autocomplete.hook.ts +++ b/packages/ui/src/core/autocomplete/autocomplete.hook.ts @@ -226,6 +226,19 @@ export function useAutocomplete( } }; + const handleSelectAllOptions = (): void => { + if (isMultiple && onChange) { + const allOptionIds = optionsList.reduce((acc, option) => { + if (!getOptionDisabled(option)) { + acc.push(getOptionValue(option)); + } + return acc; + }, []); + + onChange([...allOptionIds]); + } + }; + useEffect(() => { if (!isSearchMode) { setSearchTerm(''); @@ -276,6 +289,7 @@ export function useAutocomplete( handleOnBlur, handleKeyDown, handleClearIconClick, + handleSelectAllOptions, focusedMenuItemIndex, }; } diff --git a/packages/ui/src/core/autocomplete/autocomplete.props.ts b/packages/ui/src/core/autocomplete/autocomplete.props.ts index c101223b..afcf17e0 100644 --- a/packages/ui/src/core/autocomplete/autocomplete.props.ts +++ b/packages/ui/src/core/autocomplete/autocomplete.props.ts @@ -1,3 +1,5 @@ +import { SyntheticEvent } from 'react'; + import type { Values } from '../../utils/types'; import type { LabelProps } from '../label'; import type { MenuProps } from '../menu'; @@ -48,6 +50,10 @@ export interface AutocompleteProps extends Omit void; + onSelectAll?: (event: SyntheticEvent) => void; onChange?: (newValue: AutocompleteValueType) => void; onOpen?: (event: React.SyntheticEvent) => void; onClose?: ( @@ -165,6 +171,7 @@ export interface AutocompleteHookReturn { handleOnBlur: () => void; handleKeyDown: (event: React.KeyboardEvent) => void; handleClearIconClick: () => void; + handleSelectAllOptions: () => void; searchTerm: string; focusedMenuItemIndex: number; } diff --git a/packages/ui/src/core/autocomplete/autocomplete.styles.ts b/packages/ui/src/core/autocomplete/autocomplete.styles.ts index 6ea634d4..63bcc0d6 100644 --- a/packages/ui/src/core/autocomplete/autocomplete.styles.ts +++ b/packages/ui/src/core/autocomplete/autocomplete.styles.ts @@ -9,6 +9,7 @@ import type { WithTheme } from '../../theme/entity'; import { Color as PColor } from '../../utils/types/palette'; import StyledTag from '../tag/tag.styles'; import { AutocompleteProps } from './autocomplete.props'; +import arrowTick from '../arrow-tick'; const baseClassName = 'Autocomplete'; @@ -80,6 +81,11 @@ const OptionGroup = styled.div.attrs({ } `; +const Arrow = styled(arrowTick)` + margin-left: auto; + cursor: pointer; +`; + const Styled = applyDisplayNames({ Autocomplete, AutocompleteContainer, @@ -88,6 +94,7 @@ const Styled = applyDisplayNames({ InputIconEndContainer, Menu, OptionGroup, + Arrow, }); export default Styled; diff --git a/packages/ui/src/core/input/input.component.tsx b/packages/ui/src/core/input/input.component.tsx index c8184ace..bcce77fc 100644 --- a/packages/ui/src/core/input/input.component.tsx +++ b/packages/ui/src/core/input/input.component.tsx @@ -41,6 +41,7 @@ const Input = intrinsicComponent( iconStart, iconEnd, iconChange, + inputActions, clearIcon, iconClickStart, iconClickEnd, @@ -195,6 +196,11 @@ const Input = intrinsicComponent( ); }; + const renderInputActions = (inputActions: React.ReactNode): JSX.Element | undefined => { + if (!inputActions) return; + return {inputActions}; + }; + return ( ( $error={error} clearIcon={clearIcon} isHovering={rest.isHovering} - $isSelectedItems={Boolean(isSelectedItems)} + $isSelectedItems={Boolean(isSelectedItems || inputActions)} {...(InputPropsData || {})} > - {renderIcon(iconStart, 'start')} - {renderField()} - {renderCopyIcon()} - {showCopyMessage && renderCopyText()} - {renderClearIcon()} - {inputType === Type.Password && renderPasswordIcon()} - {renderIcon(iconEnd, 'end')} - {renderIcon(iconChange, '')} - {children && <>{children}} + + {renderIcon(iconStart, 'start')} + {renderField()} + {renderCopyIcon()} + {showCopyMessage && renderCopyText()} + {renderClearIcon()} + {inputType === Type.Password && renderPasswordIcon()} + {renderIcon(iconEnd, 'end')} + {renderIcon(iconChange, '')} + {children && <>{children}} + + + {renderInputActions(inputActions)} ); } diff --git a/packages/ui/src/core/input/input.props.ts b/packages/ui/src/core/input/input.props.ts index f16b7db9..642ad9fe 100644 --- a/packages/ui/src/core/input/input.props.ts +++ b/packages/ui/src/core/input/input.props.ts @@ -18,6 +18,7 @@ export interface InputProps extends Omit css` position: relative; - display: inline-flex; - align-items: center; + display: flex; + flex-direction: column; + align-items: stretch; box-sizing: border-box; - column-gap: 8px; cursor: text; transition: all 100ms ease-out; width: ${$fullWidth ? '100%' : '300px'}; @@ -110,7 +110,6 @@ const Input = styled.div.attrs({ background-color: ${getInputBackgroundColor(readOnly, disabled)}; border-radius: ${theme.shape.borderRadius[BRSize.Md]}; border: 1px solid ${getInputBorderColor(readOnly, disabled)}; - color: ${disabled ? theme.palette[PColor.TextPlaceholder] : theme.palette[PColor.TextPrimary]}; ${sizeInputMixin[size]} @@ -260,6 +259,23 @@ const FieldWrapper = styled.div.attrs({ ` ); +const InputActions = styled.div.attrs({ + className: generateClassNames(baseClassName, 'inputActions'), +})` + padding-top: 16px; + width: 100%; + box-sizing: border-box; +`; + +const InputContent = styled.div.attrs({ + className: generateClassNames(baseClassName, 'inputContent'), +})` + display: flex; + align-items: center; + width: 100%; + box-sizing: border-box; +`; + const Styled = applyDisplayNames({ Input, Container, @@ -272,6 +288,8 @@ const Styled = applyDisplayNames({ NotificationIcon, NotificationText, FieldWrapper, + InputActions, + InputContent, }); export default Styled;