Skip to content

Commit

Permalink
Merge pull request #161 from scaleflex/SHA-57-UI-KIT-Select-all-to-ch…
Browse files Browse the repository at this point in the history
…oose-all-applicable-values

feat: UI KIT Select all to choose all applicable values [SHA-57]
  • Loading branch information
amrelbialy authored Sep 3, 2024
2 parents d9e9933 + 81ccc70 commit b061011
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 36 deletions.
83 changes: 61 additions & 22 deletions packages/ui/src/core/autocomplete/autocomplete.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<AutocompleteProps, HTMLDivElement>(
(props: AutocompleteProps, ref): JSX.Element => {
Expand Down Expand Up @@ -49,6 +48,10 @@ const Autocomplete = intrinsicComponent<AutocompleteProps, HTMLDivElement>(
renderTag,
renderMenuItem: renderMenuItemCustomFn,
onChange,
selectAllButtonLabel = 'Select all',
clearAllButtonLabel = 'Clear all',
onClearAll,
onSelectAll,
...rest
} = props;
const {
Expand All @@ -71,6 +74,7 @@ const Autocomplete = intrinsicComponent<AutocompleteProps, HTMLDivElement>(
handleKeyDown,
handleClearIconClick,
checkIsIdSelected,
handleSelectAllOptions,
getOptionById,
focusedMenuItemIndex,
}: Partial<AutocompleteHookReturn> = useAutocomplete({
Expand All @@ -82,6 +86,24 @@ const Autocomplete = intrinsicComponent<AutocompleteProps, HTMLDivElement>(
});
const isMultiple = Boolean(multiple) && Array.isArray(formattedValue);

const handleSelectAll = (event: React.MouseEvent<HTMLElement>): void => {
handleSelectAllOptions();
event.stopPropagation();

if (onSelectAll) {
onSelectAll(event);
}
};

const handleClearAll = (event: React.MouseEvent<HTMLElement>): 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);
Expand Down Expand Up @@ -191,6 +213,41 @@ const Autocomplete = intrinsicComponent<AutocompleteProps, HTMLDivElement>(
);
};

const renderInputEndIcons = (): React.ReactNode => (
<>
{renderLabelIconEnd &&
renderLabelIconEnd({
isMultiple,
option:
!isMultiple && formattedValue && typeof formattedValue === 'string'
? getOptionById(formattedValue)
: null,
})}

{!hideArrow && (
<Styled.Arrow
{...(!disabled && !readOnly ? { onClick: handleOpenMenuClick } : {})}
type={open ? 'top' : 'bottom'}
IconProps={{ size: size === 'md' ? 11 : 10 }}
/>
)}
</>
);

const renderInputActions = (): React.ReactNode => (
<Styled.InputIconEndContainer>
<>
<Button size="sm" color="link-basic-primary" onClick={handleSelectAll}>
{selectAllButtonLabel}
</Button>
<Button color="link-secondary" size="sm" onClick={handleClearAll}>
{clearAllButtonLabel}
</Button>
</>
{renderInputEndIcons()}
</Styled.InputIconEndContainer>
);

return (
<Styled.Autocomplete ref={ref} {...rest}>
{renderLabel({ label, error, size, LabelProps: LabelPropsData })}
Expand All @@ -212,26 +269,8 @@ const Autocomplete = intrinsicComponent<AutocompleteProps, HTMLDivElement>(
placeholder={placeholder}
fullWidth={fullWidth}
isEllipsis
iconEnd={() => (
<Styled.InputIconEndContainer>
{renderLabelIconEnd &&
renderLabelIconEnd({
isMultiple,
option:
!isMultiple && formattedValue && typeof formattedValue === 'string'
? getOptionById(formattedValue)
: null,
})}

{!hideArrow && (
<ArrowTick
{...(!disabled && !readOnly ? { onClick: handleOpenMenuClick } : {})}
type={open ? 'top' : 'bottom'}
IconProps={{ size: size === Size.Md ? 11 : 10 }}
/>
)}
</Styled.InputIconEndContainer>
)}
iconEnd={isMultiple ? undefined : renderInputEndIcons}
inputActions={isMultiple ? renderInputActions() : undefined}
{...(showClearIcon
? {
clearIcon: isValueSelected && <Styled.CrossIcon size={size === 'md' ? 11 : 10} />,
Expand Down
14 changes: 14 additions & 0 deletions packages/ui/src/core/autocomplete/autocomplete.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,19 @@ export function useAutocomplete(
}
};

const handleSelectAllOptions = (): void => {
if (isMultiple && onChange) {
const allOptionIds = optionsList.reduce<AutocompleteOptionIdType[]>((acc, option) => {
if (!getOptionDisabled(option)) {
acc.push(getOptionValue(option));
}
return acc;
}, []);

onChange([...allOptionIds]);
}
};

useEffect(() => {
if (!isSearchMode) {
setSearchTerm('');
Expand Down Expand Up @@ -276,6 +289,7 @@ export function useAutocomplete(
handleOnBlur,
handleKeyDown,
handleClearIconClick,
handleSelectAllOptions,
focusedMenuItemIndex,
};
}
7 changes: 7 additions & 0 deletions packages/ui/src/core/autocomplete/autocomplete.props.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { SyntheticEvent } from 'react';

import type { Values } from '../../utils/types';
import type { LabelProps } from '../label';
import type { MenuProps } from '../menu';
Expand Down Expand Up @@ -48,6 +50,10 @@ export interface AutocompleteProps extends Omit<React.HTMLAttributes<HTMLDivElem
error?: boolean;
sortAlphabetically?: boolean;
focusOnOpen?: boolean;
clearAllButtonLabel?: string;
selectAllButtonLabel?: string;
onClearAll?: (event: SyntheticEvent) => void;
onSelectAll?: (event: SyntheticEvent) => void;
onChange?: (newValue: AutocompleteValueType) => void;
onOpen?: (event: React.SyntheticEvent<HTMLElement>) => void;
onClose?: (
Expand Down Expand Up @@ -165,6 +171,7 @@ export interface AutocompleteHookReturn {
handleOnBlur: () => void;
handleKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void;
handleClearIconClick: () => void;
handleSelectAllOptions: () => void;
searchTerm: string;
focusedMenuItemIndex: number;
}
7 changes: 7 additions & 0 deletions packages/ui/src/core/autocomplete/autocomplete.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -80,6 +81,11 @@ const OptionGroup = styled.div.attrs({
}
`;

const Arrow = styled(arrowTick)`
margin-left: auto;
cursor: pointer;
`;

const Styled = applyDisplayNames({
Autocomplete,
AutocompleteContainer,
Expand All @@ -88,6 +94,7 @@ const Styled = applyDisplayNames({
InputIconEndContainer,
Menu,
OptionGroup,
Arrow,
});

export default Styled;
30 changes: 20 additions & 10 deletions packages/ui/src/core/input/input.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const Input = intrinsicComponent<InputProps, HTMLInputElement>(
iconStart,
iconEnd,
iconChange,
inputActions,
clearIcon,
iconClickStart,
iconClickEnd,
Expand Down Expand Up @@ -195,6 +196,11 @@ const Input = intrinsicComponent<InputProps, HTMLInputElement>(
);
};

const renderInputActions = (inputActions: React.ReactNode): JSX.Element | undefined => {
if (!inputActions) return;
return <Styled.InputActions>{inputActions}</Styled.InputActions>;
};

return (
<Styled.Input
onClick={focusOnClick ? handleFocus : undefined}
Expand All @@ -210,18 +216,22 @@ const Input = intrinsicComponent<InputProps, HTMLInputElement>(
$error={error}
clearIcon={clearIcon}
isHovering={rest.isHovering}
$isSelectedItems={Boolean(isSelectedItems)}
$isSelectedItems={Boolean(isSelectedItems || inputActions)}
{...(InputPropsData || {})}
>
{renderIcon(iconStart, 'start')}
{renderField()}
{renderCopyIcon(<CopyOutline size={getIconSize(size, 'copy')} />)}
{showCopyMessage && renderCopyText()}
{renderClearIcon()}
{inputType === Type.Password && renderPasswordIcon()}
{renderIcon(iconEnd, 'end')}
{renderIcon(iconChange, '')}
{children && <>{children}</>}
<Styled.InputContent>
{renderIcon(iconStart, 'start')}
{renderField()}
{renderCopyIcon(<CopyOutline size={getIconSize(size, 'copy')} />)}
{showCopyMessage && renderCopyText()}
{renderClearIcon()}
{inputType === Type.Password && renderPasswordIcon()}
{renderIcon(iconEnd, 'end')}
{renderIcon(iconChange, '')}
{children && <>{children}</>}
</Styled.InputContent>

{renderInputActions(inputActions)}
</Styled.Input>
);
}
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/core/input/input.props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElem
size?: InputSizeType;
iconStart?: React.ReactNode | IconFuncType;
iconEnd?: React.ReactNode | IconFuncType;
inputActions?: React.ReactNode;
iconChange?: React.ReactNode | IconFuncType;
clearIcon?: React.ReactNode | IconFuncType;
iconType?: string;
Expand Down
26 changes: 22 additions & 4 deletions packages/ui/src/core/input/input.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,18 +99,17 @@ const Input = styled.div.attrs({
theme,
}) => 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'};
pointer-events: ${disabled ? 'none' : 'auto'};
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]}
Expand Down Expand Up @@ -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,
Expand All @@ -272,6 +288,8 @@ const Styled = applyDisplayNames({
NotificationIcon,
NotificationText,
FieldWrapper,
InputActions,
InputContent,
});

export default Styled;

0 comments on commit b061011

Please sign in to comment.