Skip to content

Commit

Permalink
feat(TDC-7378): New enumeration component
Browse files Browse the repository at this point in the history
  • Loading branch information
dlcaldeira committed Nov 22, 2023
1 parent 018d449 commit 91c789e
Show file tree
Hide file tree
Showing 15 changed files with 542 additions and 513 deletions.
5 changes: 5 additions & 0 deletions .changeset/orange-seahorses-talk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@talend/design-system': minor
---

feat(TDC-7378): New enumeration component
1 change: 0 additions & 1 deletion packages/design-system/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
"@talend/assets-api": "^1.3.0",
"@talend/design-tokens": "^2.10.0",
"@talend/utils": "^2.8.0",
"@types/react-virtualized": "^9.21.26",
"classnames": "^2.3.2",
"keycode": "^2.2.1",
"modern-css-reset": "^1.4.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { AutoSizer, InfiniteLoader, List } from 'react-virtualized';
import { EmptyState } from '../EmptyState';
import { StackHorizontal } from '../Stack';
import { I18N_DOMAIN_DESIGN_SYSTEM } from '../constants';
import { EnumerationMode, EnumerationProps } from './Enumeration.types';
import { EnumerationMode, EnumerationProps, UiEnumerationItem } from './Enumeration.types';
import { EnumerationHeader } from './EnumerationHeader/EnumerationHeader.component';
import { EnumerationItem } from './EnumerationItem/EnumerationItem.component';

Expand All @@ -26,6 +26,41 @@ export const Enumeration = ({
const [mode, setMode] = useState(EnumerationMode.VIEW);
const [selectedItems, setSelectedItems] = useState<string[]>([]);
const [filteredItems, setFilteredItems] = useState(items);
const [scrollToIndex, setScrollToIndex] = useState<number>();
const [uiItems, setUiItems] = useState<UiEnumerationItem[]>(
filteredItems.map(item => ({
value: item,
isToAnimate: false,
})),
);

const setIsToAnimate = (itemsToAnimate: string[], isToAnimate: boolean) => {
const newItems = [...uiItems];

itemsToAnimate.forEach(itemToAnimate => {
const indextToUpdate = newItems.findIndex(item => item.value === itemToAnimate);

if (indextToUpdate < 0) {
newItems.unshift({
value: itemToAnimate,
isToAnimate,
});
} else {
newItems[indextToUpdate].isToAnimate = isToAnimate;
}
});

setUiItems(newItems);
};

const onAnimate = (newItems: string[]) => {
setScrollToIndex(0);
setIsToAnimate(newItems, true);
setTimeout(() => {
setIsToAnimate(newItems, false);
setScrollToIndex(undefined);
}, 2500);
};

return (
<div className={styles.enumeration}>
Expand All @@ -34,9 +69,16 @@ export const Enumeration = ({
id={id}
items={items}
mode={mode}
onChange={onChange}
onChange={(newItem: string) => {
onChange([newItem, ...items]);
onAnimate([newItem]);
}}
onCreate={onCreate}
onImport={onImport}
onImport={(data: string) => {
const newItems = data.split('\n').filter(Boolean);
onImport?.(newItems);
onAnimate(newItems);
}}
onRemove={onRemove}
selectedItems={selectedItems}
setFilteredItems={setFilteredItems}
Expand All @@ -46,26 +88,29 @@ export const Enumeration = ({
/>

{filteredItems.length ? (
<div className={styles.enumeration__body}>
<AutoSizer disableHeight={true}>
{({ width }) => {
const itemHeight = 38;

return (
<InfiniteLoader
isRowLoaded={({ index }) => !!items[index]}
loadMoreRows={loadMoreRows}
rowCount={filteredItems.length}
>
{({ onRowsRendered, registerChild }) => (
<List
height={filteredItems.length * itemHeight}
onRowsRendered={onRowsRendered}
ref={registerChild}
rowCount={filteredItems.length}
rowHeight={itemHeight}
rowRenderer={({ index }) => (
<InfiniteLoader
isRowLoaded={({ index }) => !!filteredItems[index]}
loadMoreRows={loadMoreRows}
rowCount={filteredItems.length}
>
{({ onRowsRendered, registerChild }) => {
const itemHeight = 38;
return (
<AutoSizer disableHeight={true}>
{({ width }) => (
<List
scrollToIndex={scrollToIndex}
height={Math.min(filteredItems.length * itemHeight, 400)}
onRowsRendered={onRowsRendered}
overscanRowCount={25}
ref={registerChild}
rowCount={filteredItems.length}
rowHeight={itemHeight}
rowRenderer={({ index, key, style }) => (
<div style={style}>
<EnumerationItem
isToAnimate={uiItems[index]?.isToAnimate}
key={key}
mode={mode}
onChange={value => {
const indexToReplace = items.indexOf(filteredItems[index]);
Expand All @@ -81,15 +126,15 @@ export const Enumeration = ({
setSelectedItems={setSelectedItems}
value={filteredItems[index]}
/>
)}
width={width}
/>
)}
</InfiniteLoader>
);
}}
</AutoSizer>
</div>
</div>
)}
width={width}
/>
)}
</AutoSizer>
);
}}
</InfiniteLoader>
) : (
<StackHorizontal gap={0} padding={{ x: 0, y: 'M' }}>
<EmptyState
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,4 @@
white-space: nowrap;
padding-right: tokens.$coral-spacing-xxs;
}

&__body {
max-height: 400px;
overflow: hidden auto;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ export interface EnumerationProps {
title: string;
}

export interface UiEnumerationItem {
value: string;
isToAnimate: boolean;
}

export enum EnumerationMode {
CREATE = 'create',
EDIT = 'edit',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,12 @@ export const EnumerationHeader = ({
const importRef = useRef<HTMLInputElement>(null);
const [searchValue, setSearchValue] = useState<string>();
const [hasError, setHasError] = useState(false);
const enumerationItemsRef = useRef<HTMLDivElement>(null);

const filterList = () => {
if (searchValue) {
setFilteredItems(items.filter(item => item.includes(searchValue)));
} else {
setFilteredItems(items);
}
};

const handleOnCreate = async (newValue?: string) => {
try {
if (newValue) {
await onCreate?.(newValue);
enumerationItemsRef.current?.scrollTo({
top: 0,
behavior: 'smooth',
});
onChange([newValue, ...items]);
onChange(newValue);
}
} catch (e) {
//The parent component must do the error handling
Expand All @@ -59,16 +46,14 @@ export const EnumerationHeader = ({

const isAllChecked = () => selectedItems.length > 0 || selectedItems.length === items.length;

const onToggleAll = (isChecked: boolean) => {
if (isChecked) {
setSelectedItems(items);
} else {
setSelectedItems([]);
}
};
const onToggleAll = (isChecked: boolean) => setSelectedItems(isChecked ? items : []);

useEffect(() => {
filterList();
if (searchValue) {
setFilteredItems(items.filter(item => item.includes(searchValue)));
} else {
setFilteredItems(items);
}
}, [searchValue, items]);

Check warning on line 57 in packages/design-system/src/components/Enumeration/EnumerationHeader/EnumerationHeader.component.tsx

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/design-system/src/components/Enumeration/EnumerationHeader/EnumerationHeader.component.tsx#L57

[react-hooks/exhaustive-deps] React Hook useEffect has a missing dependency: 'setFilteredItems'. Either include it or remove the dependency array. If 'setFilteredItems' changes too often, find the parent component that defines it and wrap that definition in useCallback.

return (
Expand Down Expand Up @@ -136,7 +121,7 @@ export const EnumerationHeader = ({

if (file) {
fr.readAsText(file);
fr.onload = () => onImport(fr.result);
fr.onload = () => onImport(fr.result as string);
}
}}
/>
Expand All @@ -157,7 +142,7 @@ export const EnumerationHeader = ({
{mode === EnumerationMode.CREATE ? (
<StackVertical gap={'XXS'} align={'stretch'}>
<InlineEditing.Text
isEditionMode={true}
isEditMode={true}
label={t('ENUMERATION_ADD_INPUT_PLACEHOLDER', 'Enter a value')}
placeholder={t('ENUMERATION_ADD_INPUT_PLACEHOLDER', 'Enter a value')}
onEdit={(_, value) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ export interface EnumerationHeaderProps {
id: string;
items: string[];
mode: EnumerationMode;
setFilteredItems: (filteredItems: string[]) => void;
setMode: (mode: EnumerationMode) => void;
onChange: any;
onChange: (item: string) => void;
onCreate?: (value: string) => Promise<unknown>;
onImport?: (...params: unknown[]) => void;
onImport?: (data: string) => void;
onRemove?: (entries: string[]) => Promise<unknown>;
selectedItems: string[];
setFilteredItems: (filteredItems: string[]) => void;
setMode: (mode: EnumerationMode) => void;
setSelectedItems: (selectedItems: string[]) => void;
title: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import { ButtonIcon } from '../../ButtonIcon';
import { Dropdown } from '../../Dropdown';
import { Form } from '../../Form';
import { InlineEditing } from '../../InlineEditing';
import { Skeleton } from '../../Skeleton';
import { I18N_DOMAIN_DESIGN_SYSTEM } from '../../constants';
import { EnumerationMode } from '../Enumeration.types';
import { EnumerationItemProps } from './EnumerationItem.types';

import styles from './EnumerationItem.module.scss';

export const EnumerationItem = ({
isToAnimate,
mode,
onChange,
onRemove,
Expand All @@ -24,38 +26,38 @@ export const EnumerationItem = ({
}: EnumerationItemProps) => {
const { t } = useTranslation(I18N_DOMAIN_DESIGN_SYSTEM);
const id = useId();

const [isEdit, setIsEdit] = useState(false);

const isChecked = (id: string) => selectedItems.includes(id);
const isChecked = (itemId: string) => selectedItems.includes(itemId);

const onToggleItem = (value: string, isChecked: boolean) => {
if (isChecked) {
setSelectedItems([...selectedItems, value]);
} else {
setSelectedItems(selectedItems.filter(item => item !== value));
}
const onToggleItem = (itemValue: string, isItemChecked: boolean) => {
setSelectedItems(
isItemChecked
? [...selectedItems, itemValue]
: selectedItems.filter(item => item !== itemValue),
);
};

return (
<div
className={classNames(styles['enumeration__item'], {
className={classNames(styles.enumeration__item, {
[styles['enumeration__item--edit']]: isEdit,
[styles['enumeration__item--animate']]: isToAnimate,
})}
>
{isEdit ? (
<InlineEditing.Text
defaultValue={value}
isEditionMode={true}
isEditMode={true}
label={`${value}`}
onCancel={() => setIsEdit(false)}
onEdit={(_, value) => {
onChange(value);
onEdit={(_, newValue) => {
onChange(newValue);
setIsEdit(false);
}}
placeholder={value}
/>
) : (
) : value ? (
<>
{mode === EnumerationMode.EDIT ? (
<Form.Checkbox
Expand All @@ -79,7 +81,7 @@ export const EnumerationItem = ({
{
label: t('ENUMERATION_DROPDOWN_REMOVE', 'Remove'),
icon: 'trash',
onClick: async () => await onRemove?.([value]),
onClick: () => onRemove?.([value]),
type: 'button',
},
]}
Expand All @@ -94,6 +96,8 @@ export const EnumerationItem = ({
</ButtonIcon>
</Dropdown>
</>
) : (
<Skeleton variant="paragraph" />
)}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
display: flex;
height: 38px;
justify-content: space-between;
padding: tokens.$coral-spacing-xxs tokens.$coral-spacing-m tokens.$coral-spacing-xxs tokens.$coral-spacing-s;
padding: tokens.$coral-spacing-xxs tokens.$coral-spacing-xs tokens.$coral-spacing-xxs tokens.$coral-spacing-s;
transition: background-color tokens.$coral-transition-fast;

&:hover {
Expand All @@ -22,6 +22,20 @@
}

&--edit {
padding: 0 tokens.$coral-spacing-m 0 tokens.$coral-spacing-xxs;
padding: 0 tokens.$coral-spacing-xxs;
}

&--animate {
animation: highlight 1500ms both 100ms;
}
}

@keyframes highlight {
0% {
background-color: tokens.$coral-color-neutral-background;
}

75% {
background-color: tokens.$coral-color-accent-background;
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { EnumerationMode } from '../Enumeration.types';

export interface EnumerationItemProps {
isToAnimate: boolean;
mode: EnumerationMode;
onChange: (items: string) => void;
onRemove?: (entries: string[]) => Promise<unknown>;
onChange: (item: string) => void;
onRemove?: (entries: string[]) => void;
selectedItems: string[];
setSelectedItems: (selectedItems: string[]) => void;
value: string;
Expand Down
Loading

0 comments on commit 91c789e

Please sign in to comment.