Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ListV2): create ListV2 component #165

Merged
merged 1 commit into from
Feb 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/components/display/ListItem.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
The ListItem is the component to use inside List (V2).

Differently from the old version of the list, the ListItem is not memoized.
It's up to the external usage the memoization.

Refer to the [ListV2](#/Components/Data%20display/ListV2) documentation for examples
71 changes: 71 additions & 0 deletions src/components/display/ListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* SPDX-FileCopyrightText: 2023 Zextras <https://www.zextras.com>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React from 'react';

import styled, { DefaultTheme, SimpleInterpolation } from 'styled-components';

import { useIsVisible } from '../../hooks/useIsVisible';
import { pseudoClasses } from '../../theme/theme-utils';

interface ListItemWrapperProps {
/** Base background color for the item */
background?: string | keyof DefaultTheme['palette'];
/** Background color for the selected status */
selectedBackground?: string | keyof DefaultTheme['palette'];
/** Background color for the active status */
activeBackground?: string | keyof DefaultTheme['palette'];
/** Define if the item is active in order to show the activeBackground */
active?: boolean;
/** Define if the item is selected in order to show the selectedBackground */
selected?: boolean;
}

const ListItemWrapper = styled.div.attrs<
ListItemWrapperProps,
{ backgroundColor?: string | keyof DefaultTheme['palette'] }
>(({ background, selectedBackground, activeBackground, active, selected }) => ({
backgroundColor: (active && activeBackground) || (selected && selectedBackground) || background
}))`
user-select: none;
outline: none;
${({ theme, backgroundColor }): SimpleInterpolation =>
backgroundColor && pseudoClasses(theme, backgroundColor)};
`;

interface ListItemProps extends ListItemWrapperProps {
/**
* Ref of the list used to set the visibility of the item.
* The ref is set by the list itself.
*
* @ignore
*/
listRef?: React.RefObject<HTMLDivElement>;
/**
* Content of the item (render prop).
* Visible arg define if the item is visible on the screen or if is in the hidden part of the list.
* This is useful to avoid rendering components which are not visible (virtual list), replacing
* them with a placeholder which allow the scrollbar to have the right height.
*/
children: (visible: boolean) => React.ReactElement;
key: NonNullable<React.HTMLProps<HTMLDivElement>['key']>;
}

function ListItemFn(
{ listRef, children, ...rest }: ListItemProps,
ref: React.ForwardedRef<HTMLDivElement>
): JSX.Element {
const [inView, itemRef] = useIsVisible<HTMLDivElement>(listRef, ref);

return (
<ListItemWrapper tabIndex={0} ref={itemRef} {...rest}>
{children(inView)}
</ListItemWrapper>
);
}

const ListItem = React.forwardRef(ListItemFn);

export { ListItem, ListItemProps, ListItemWrapperProps };
223 changes: 223 additions & 0 deletions src/components/display/ListV2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
This is a renewed implementation of the [List](#/Components/Data%20display/List).

The items are passed as children of the List component, and the management of the selected, active and
of the other controlled statuses are left to the external Component.

The following example are the reproduction of the ones of the List, build with the new structure.

The props **background**, **activeBackground** and **selectedBackground** are still available on the main list
with their default, but be aware that they are just passed to the children.
In other words, they represent a short way to define the background on the ListItems.

```jsx
import { faker } from '@faker-js/faker';
import React, { useState, useMemo, useCallback } from 'react';
import { Container, Avatar, Drag, Divider, Text, Row, Padding, ListItem } from '@zextras/carbonio-design-system';
import { map, range, omit, some } from 'lodash';

const ListItemContent = React.memo(({
visible,
item,
selected,
selecting,
onClick
}) => {
const clickHandler = useCallback(() => {
onClick(item.id);
}, [item.id, onClick]);

return visible ? (
<Drag type="item" data={item} style={{ width: "100%" }}>
<Container
onClick={clickHandler}
height="4rem"
orientation="vertical"
mainAlignment="flex-start"
>
<Container padding={{ all: "small" }} orientation="horizontal" mainAlignment="flex-start">
<Padding horizontal="small">
<Avatar
selecting={selecting}
selected={selected}
label={item.name}
picture={item.image}
/>
</Padding>
<Container padding={{ left: "small" }} orientation="vertical" crossAlignment="flex-start">
<Row height="fit" orientation="horizontal" padding={{ bottom: "small" }}>
<Padding right="small">
<Text weight="bold">{item.name}</Text>
</Padding>
<Text>{item.email}</Text>
</Row>
<Text>{item.text}</Text>
</Container>
</Container>
<Divider />
</Container>
</Drag>
) : (
<div style={{ height: "4rem" }} />
);
});


const ListV2Example = () => {
const [selected, setSelected] = useState({});

const toggleSelect = useCallback((id) => {
setSelected((selectedMap) =>
selectedMap[id] ? omit(selectedMap, id) : ({ ...selectedMap, [id]: true })
);
}, []);

const selecting = some(selected, (isSelected) => isSelected);

const data = useMemo(
() =>
map(range(0, 500), (i) => ({
id: `${i}`,
name: faker.name.fullName(),
email: faker.internet.email(),
text: faker.lorem.sentence(),
image: faker.image.avatar()
})),
[]
);

const items = useMemo(
() =>
map(data, (item) => (
<ListItem key={item.id} selected={selected[item.id]} active={item.id === '2'}>
{(visible) => (
<ListItemContent
visible={visible}
item={item}
selected={selected[item.id]}
selecting={selecting}
onClick={toggleSelect}
/>
)}
</ListItem>
)),
[data, selected, selecting, toggleSelect]
);

return (
<Container height={"750px"}>
<ListV2>{items}</ListV2>
</Container>
);
};

<ListV2Example />
```


### List with background properties

```jsx
import { faker } from '@faker-js/faker';
import React, { useState, useMemo, useCallback } from 'react';
import { Container, Avatar, Drag, Divider, Text, Row, Padding, ListItem } from '@zextras/carbonio-design-system';
import { map, range, omit, some } from 'lodash';

const ListItemContent = React.memo(({
visible,
item,
selected,
selecting,
onClick
}) => {
const clickHandler = useCallback(() => {
onClick(item.id);
}, [item.id, onClick]);

return visible ? (
<Drag type="item" data={item} style={{ width: "100%" }}>
<Container
onClick={clickHandler}
height="4rem"
orientation="vertical"
mainAlignment="flex-start"
>
<Container padding={{ all: "small" }} orientation="horizontal" mainAlignment="flex-start">
<Padding horizontal="small">
<Avatar
selecting={selecting}
selected={selected}
label={item.name}
picture={item.image}
/>
</Padding>
<Container padding={{ left: "small" }} orientation="vertical" crossAlignment="flex-start">
<Row height="fit" orientation="horizontal" padding={{ bottom: "small" }}>
<Padding right="small">
<Text weight="bold">{item.name}</Text>
</Padding>
<Text>{item.email}</Text>
</Row>
<Text>{item.text}</Text>
</Container>
</Container>
<Divider />
</Container>
</Drag>
) : (
<div style={{ height: "4rem" }} />
);
});


const ListV2Example = () => {
const [selected, setSelected] = useState({});

const toggleSelect = useCallback((id) => {
setSelected((selectedMap) =>
selectedMap[id] ? omit(selectedMap, id) : ({ ...selectedMap, [id]: true })
);
}, []);

const selecting = some(selected, (isSelected) => isSelected);

const data = useMemo(
() =>
map(range(0, 500), (i) => ({
id: `${i}`,
name: faker.name.fullName(),
email: faker.internet.email(),
text: faker.lorem.sentence(),
image: faker.image.avatar()
})),
[]
);

const items = useMemo(
() =>
map(data, (item) => (
<ListItem key={item.id} selected={selected[item.id]} active={item.id === '2'}>
{(visible) => (
<ListItemContent
visible={visible}
item={item}
selected={selected[item.id]}
selecting={selecting}
onClick={toggleSelect}
/>
)}
</ListItem>
)),
[data, selected, selecting, toggleSelect]
);

return (
<Container height={"250px"}>
<ListV2 selectedBackground="success" activeBackground="warning">
{items}
</ListV2>
</Container>
);
};

<ListV2Example />
```
71 changes: 71 additions & 0 deletions src/components/display/ListV2.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* SPDX-FileCopyrightText: 2022 Zextras <https://www.zextras.com>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React from 'react';

import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { render } from '../../test-utils';
import { Container } from '../layout/Container';
import { ListItem } from './ListItem';
import { ListV2 } from './ListV2';

describe('List', () => {
test('Render a basic list', () => {
const items = [
{
id: '1',
name: 'item 1'
},
{
id: '2',
name: 'item 2'
}
];

const listItems = items.map((item) => (
<ListItem key={item.id}>{(): JSX.Element => <div>{item.name}</div>}</ListItem>
));

render(<ListV2>{listItems}</ListV2>);

expect(screen.getByText('item 1')).toBeVisible();
expect(screen.getByText('item 2')).toBeVisible();
});

test('Render a list with a clickable item', () => {
const items = [
{
id: '1',
name: 'item 1',
onClick: jest.fn()
},
{
id: '2',
name: 'item 2',
onClick: jest.fn()
}
];

const listItems = items.map((item) => (
<ListItem key={item.id}>
{(): JSX.Element => (
<Container key={item.id} onClick={item.onClick}>
{item.name}
</Container>
)}
</ListItem>
));

render(<ListV2>{listItems}</ListV2>);

expect(screen.getByText('item 1')).toBeVisible();
expect(screen.getByText('item 2')).toBeVisible();
userEvent.click(screen.getByText('item 1'));
expect(items[0].onClick).toHaveBeenCalled();
expect(items[1].onClick).not.toHaveBeenCalled();
});
});
Loading