Skip to content

Commit

Permalink
feat(select-with-tags): added collapsed tag list (#511)
Browse files Browse the repository at this point in the history
* feat(select-with-tags): added collapsed tag list

* feat(select-with-tags): fixed remarks

* feat(select-with-tags): collapse on delete, close, add

* feat(select-with-tags): fixed remarks

* feat(select-with-tags): fixed input min width

* feat(select-with-tags): fixed remarks

* feat(select-with-tags): added transformation text tag function

* feat(select-with-tags): added transformation text tag function

* feat(select-with-tags): fixed remarks

Co-authored-by: Демичев Андрей Валерьевич <ADemichev@alfabank.ru>
  • Loading branch information
nekipa12 and Демичев Андрей Валерьевич committed Feb 19, 2021
1 parent 848106d commit fe1d551
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 27 deletions.
8 changes: 7 additions & 1 deletion packages/select-with-tags/src/component.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,15 @@ export const matchOption = (option, inputValue) =>
};
const handleChange = React.useCallback(({ selectedMultiple }) => {
setSelected(selectedMultiple);
}, [])
}, []);
const transformCollapsedTagText = (collapsedCount) => {
return `+${collapsedCount} element`;
};
return (
<div style={{width: '375px'}}>
<SelectWithTags
collapseTagList={boolean('collapseTagList', true)}
moveInputToNewLine={boolean('moveInputToNewLine', true)}
options={options}
block={boolean('block', true)}
size={select('size', ['s', 'm', 'l'], 'l')}
Expand All @@ -68,6 +73,7 @@ export const matchOption = (option, inputValue) =>
label={text('label', '')}
autocomplete={boolean('autocomplete', true)}
onInput={handleInput}
transformCollapsedTagText={transformCollapsedTagText}
value={value}
onChange={handleChange}
selected={selected}
Expand Down
22 changes: 21 additions & 1 deletion packages/select-with-tags/src/component.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { ChangeEvent, forwardRef, useCallback, useState } from 'react';
import React, { ChangeEvent, forwardRef, useCallback, useState, useRef } from 'react';
import {
BaseSelectProps,
OptionsList as DefaultOptionsList,
Expand Down Expand Up @@ -28,7 +28,11 @@ export const SelectWithTags = forwardRef<HTMLInputElement, SelectWithTagsProps>(
autocomplete = true,
match,
allowUnselect = true,
collapseTagList = false,
moveInputToNewLine = true,
emptyListPlaceholder = 'Ничего не найдено',
transformCollapsedTagText,
transformTagText,
Tag,
...restProps
},
Expand All @@ -37,13 +41,21 @@ export const SelectWithTags = forwardRef<HTMLInputElement, SelectWithTagsProps>(
const controlled = Boolean(selected);

const [selectedTags, setSelectedTags] = useState(selected || []);
const [isPopoverOpen, setPopoverOpen] = useState<boolean | undefined>(false);
const updatePopover = useRef(() => null);

const resetValue = useCallback(() => {
const event = { target: { value: '' } };

onInput(event as ChangeEvent<HTMLInputElement>);
}, [onInput]);

const handleUpdatePopover = useCallback(() => {
if (updatePopover && updatePopover.current) {
updatePopover.current();
}
}, []);

const handleDeleteTag = useCallback(
(deletedKey: string) => {
let tags = selected || selectedTags;
Expand Down Expand Up @@ -87,6 +99,7 @@ export const SelectWithTags = forwardRef<HTMLInputElement, SelectWithTagsProps>(
if (!open && value) {
resetValue();
}
setPopoverOpen(open);
},
[resetValue, value],
);
Expand All @@ -105,6 +118,7 @@ export const SelectWithTags = forwardRef<HTMLInputElement, SelectWithTagsProps>(
OptionsList={OptionsList}
Arrow={Arrow}
multiple={true}
updatePopover={updatePopover}
allowUnselect={allowUnselect}
showEmptyOptionsList={true}
fieldProps={{
Expand All @@ -113,6 +127,12 @@ export const SelectWithTags = forwardRef<HTMLInputElement, SelectWithTagsProps>(
onInput,
handleDeleteTag,
Tag,
collapseTagList,
moveInputToNewLine,
transformCollapsedTagText,
transformTagText,
handleUpdatePopover,
isPopoverOpen,
}}
optionsListProps={{
emptyPlaceholder: emptyListPlaceholder,
Expand Down
113 changes: 96 additions & 17 deletions packages/select-with-tags/src/components/tag-list/component.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,39 @@
import React, {
ChangeEvent,
FC,
MutableRefObject,
useCallback,
useRef,
useMemo,
useState,
KeyboardEventHandler,
MouseEventHandler,
ReactNode,
useEffect,
ChangeEvent,
useCallback,
useLayoutEffect,
MutableRefObject,
MouseEventHandler,
KeyboardEventHandler,
} from 'react';
import cn from 'classnames';
import { useFocus } from '@alfalab/hooks';
import { FieldProps } from '@alfalab/core-components-select';
import { FormControl, FormControlProps } from '@alfalab/core-components-form-control';
import { useFocus } from '@alfalab/hooks';

import styles from './index.module.css';

import { TagComponent } from '../../types';
import { Tag as DefaultTag } from '../tag';
import { calculateTotalElementsPerRow } from '../../utils/calculate-collapse-size';
import styles from './index.module.css';

type TagListOwnProps = {
value?: string;
handleDeleteTag?: (key: string) => void;
onInput?: (event: ChangeEvent<HTMLInputElement>) => void;
inputRef?: MutableRefObject<HTMLInputElement>;
autocomplete?: boolean;
isPopoverOpen?: boolean;
collapseTagList?: boolean;
moveInputToNewLine?: boolean;
transformCollapsedTagText?: (collapsedCount: number) => string;
transformTagText?: (tagText?: ReactNode) => ReactNode;
Tag?: TagComponent;
handleUpdatePopover?: () => void;
};

export const TagList: FC<FieldProps & FormControlProps & TagListOwnProps> = ({
Expand All @@ -44,10 +52,18 @@ export const TagList: FC<FieldProps & FormControlProps & TagListOwnProps> = ({
valueRenderer,
onInput,
handleDeleteTag,
collapseTagList,
moveInputToNewLine,
transformCollapsedTagText,
transformTagText,
isPopoverOpen,
handleUpdatePopover,
Tag = DefaultTag,
...restProps
}) => {
const [focused, setFocused] = useState(false);
const [isShowMoreEnabled, setShowMoreEnabled] = useState<boolean | undefined>(false);
const [visibleElements, setVisibleElements] = useState(1);
const [inputOnNewLine, setInputOnNewLine] = useState(false);

const wrapperRef = useRef<HTMLDivElement>(null);
Expand All @@ -57,6 +73,25 @@ export const TagList: FC<FieldProps & FormControlProps & TagListOwnProps> = ({
const [focusVisible] = useFocus(wrapperRef, 'keyboard');
const [inputFocusVisible] = useFocus(inputRef, 'keyboard');

useLayoutEffect(() => {
setShowMoreEnabled(isPopoverOpen);
}, [isPopoverOpen]);

useLayoutEffect(() => {
setVisibleElements(selectedMultiple.length);
setShowMoreEnabled(false);
}, [selectedMultiple]);

useLayoutEffect(() => {
if (collapseTagList && contentWrapperRef.current) {
const totalVisibleElements = calculateTotalElementsPerRow(
contentWrapperRef.current,
autocomplete && inputRef.current ? inputRef.current : null,
);
setVisibleElements(totalVisibleElements);
}
}, [collapseTagList, visibleElements, autocomplete]);

const handleFocus = useCallback(() => setFocused(true), []);
const handleBlur = useCallback(() => setFocused(false), []);

Expand Down Expand Up @@ -103,16 +138,39 @@ export const TagList: FC<FieldProps & FormControlProps & TagListOwnProps> = ({
[handleDeleteTag, selectedMultiple, value],
);

const toggleShowMoreLessButton = useCallback(
event => {
event.stopPropagation();
setShowMoreEnabled(value => !value);
if (handleUpdatePopover) {
handleUpdatePopover();
}
},
[handleUpdatePopover],
);

useEffect(() => {
/**
* Если текст не помещается в инпут, то нужно перенести инпут на новую строку.
*/
if (inputTextIsOverflow() && !inputOnNewLine) {
setInputOnNewLine(true);
} else if (value.length === 0) {
setInputOnNewLine(false);
if (moveInputToNewLine) {
if (inputTextIsOverflow() && !inputOnNewLine) {
setInputOnNewLine(true);
} else if (value.length === 0) {
setInputOnNewLine(false);
}
}
}, [value, inputOnNewLine, inputTextIsOverflow, moveInputToNewLine]);

const collapseTagTitle = useMemo(() => {
if (isShowMoreEnabled) {
return 'Свернуть';
}
}, [value, inputOnNewLine, inputTextIsOverflow]);
if (transformCollapsedTagText) {
return transformCollapsedTagText(selectedMultiple.length - visibleElements);
}
return `+${selectedMultiple.length - visibleElements}`;
}, [transformCollapsedTagText, isShowMoreEnabled, selectedMultiple.length, visibleElements]);

const filled = Boolean(selectedMultiple.length > 0) || Boolean(value);

Expand Down Expand Up @@ -148,9 +206,30 @@ export const TagList: FC<FieldProps & FormControlProps & TagListOwnProps> = ({
})}
ref={contentWrapperRef}
>
{selectedMultiple.map(option => (
<Tag key={option.key} option={option} handleDeleteTag={handleDeleteTag} />
))}
{selectedMultiple.map((option, index) =>
isShowMoreEnabled || index + 1 <= visibleElements ? (
<Tag
option={{
...option,
content: transformTagText
? transformTagText(option.content)
: option.content,
}}
key={option.key}
handleDeleteTag={handleDeleteTag}
/>
) : null,
)}
{visibleElements < selectedMultiple.length && (
<Tag
data-collapse='collapse-last-tag-element'
onClick={toggleShowMoreLessButton}
option={{
key: 'collapse',
content: collapseTagTitle,
}}
/>
)}

{autocomplete && (
<input
Expand Down
24 changes: 18 additions & 6 deletions packages/select-with-tags/src/components/tag/component.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
import React, { useCallback } from 'react';
import cn from 'classnames';
import { Tag as CoreTag } from '@alfalab/core-components-tag';
import { CrossCompactMIcon } from '@alfalab/icons-glyph/CrossCompactMIcon';

import { TagComponent } from '../../types';

import styles from './index.module.css';

export const Tag: TagComponent = ({ option: { content, key }, handleDeleteTag }) => {
export const Tag: TagComponent = ({
option: { content, key },
onClick,
handleDeleteTag,
...props
}) => {
const handleClick = useCallback(() => {
if (handleDeleteTag) {
handleDeleteTag(key);
}
}, [handleDeleteTag, key]);

return (
<CoreTag key={key} size='xs' checked={true} className={styles.tag}>
<CoreTag
key={key}
size='xs'
onClick={onClick}
checked={!!handleDeleteTag}
className={cn(styles.tag, { [styles.tagNoClose]: !handleDeleteTag })}
{...props}
>
<span className={styles.tagContentWrap}>
{content}

<CrossCompactMIcon onClick={handleClick} className={styles.tagCross} />
{handleDeleteTag && (
<CrossCompactMIcon onClick={handleClick} className={styles.tagCross} />
)}
</span>
</CoreTag>
);
Expand Down
4 changes: 4 additions & 0 deletions packages/select-with-tags/src/components/tag/index.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
}
}

.tagNoClose {
padding-right: var(--gap-m);
}

.tagContentWrap {
display: flex;
justify-content: flex-start;
Expand Down
25 changes: 23 additions & 2 deletions packages/select-with-tags/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { ChangeEvent, FC } from 'react';
import { ChangeEvent, FC, ReactNode } from 'react';
import { BaseSelectProps, OptionShape } from '@alfalab/core-components-select';
import { TagProps as TagPropsBase } from '@alfalab/core-components-tag';

export type OptionMatcher = (option: OptionShape, inputValue: string) => boolean;

export type TagProps = {
option: OptionShape;
handleDeleteTag?: (key: string) => void;
};
} & TagPropsBase;

export type TagComponent = FC<TagProps>;

Expand Down Expand Up @@ -53,4 +54,24 @@ export type SelectWithTagsProps = Omit<
* Компонент Тэг
*/
Tag?: TagComponent;

/**
* Показывать тэги только в одном ряду, а если не помещаются в один ряд - схлопнуть в одну кнопку
*/
collapseTagList?: boolean;

/**
* Если текст не помещается в инпут, то нужно перенести инпут на новую строку.
*/
moveInputToNewLine?: boolean;

/**
* Трансформировать текст компонента Тэг который отображает общее количество выбранных элементов
*/
transformCollapsedTagText?: (collapsedCount: number) => string;

/**
* Трансформировать текст компонента Тэг
*/
transformTagText?: (tagText?: ReactNode) => ReactNode;
};

0 comments on commit fe1d551

Please sign in to comment.