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

DSM Update #20386

Merged
merged 1 commit into from
Jan 10, 2024
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ You can use a dedicated component to display avatar list. After a defined maximu
<>
<Avatars
max={5}
title="Helen Doe&#10;Isabel Doe&#10;John Doe&#10;Kurt Doe&#10;Leonard Doe"
maxTitle={5}
>
<Avatar
{...args}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, {useMemo} from 'react';
import styled, {css} from 'styled-components';
import {useTheme} from '../../hooks';
import {Override} from '../../shared';
import {AkeneoThemedProps, getColor} from '../../theme';
import {AvatarProps} from './types';

const AvatarContainer = styled.span<AvatarProps & AkeneoThemedProps>`
${({size}) =>
Expand Down Expand Up @@ -31,43 +31,13 @@ const AvatarContainer = styled.span<AvatarProps & AkeneoThemedProps>`
cursor: ${({onClick}) => (onClick ? 'pointer' : 'default')};
`;

type AvatarProps = Override<
React.HTMLAttributes<HTMLSpanElement>,
{
/**
* Username to use as fallback if the avatar is not provided and the Firstname and Lastname are empty.
*/
username: string;

/**
* Firstname to use as fallback with the Lastname if the avatar is not provided.
*/
firstName: string;

/**
* Lastname to use as fallback with the Firstname if the avatar is not provided.
*/
lastName: string;

/**
* Url of the avatar image.
*/
avatarUrl?: string;

/**
* Size of the avatar.
*/
size?: 'default' | 'big';
}
>;

const Avatar = ({username, firstName, lastName, avatarUrl, size = 'default', ...rest}: AvatarProps) => {
const theme = useTheme();

const fallback = (
firstName.trim().charAt(0) + lastName.trim().charAt(0) || username.substring(0, 2)
).toLocaleUpperCase();
const title = `${firstName} ${lastName}`.trim() || username;
const title = `${firstName || ''} ${lastName || ''}`.trim() || username;

const backgroundColor = useMemo(() => {
const colorId = username.split('').reduce<number>((s, l) => s + l.charCodeAt(0), 0);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, {Children} from 'react';
import React, {Children, useMemo} from 'react';
import styled from 'styled-components';
import {Override} from '../../shared';
import {AkeneoThemedProps, getColor} from '../../theme';
import {AvatarProps} from './types';

const AvatarListContainer = styled.div<AvatarsProps & AkeneoThemedProps>`
display: flex;
Expand All @@ -16,10 +17,11 @@ const AvatarListContainer = styled.div<AvatarsProps & AkeneoThemedProps>`
const RemainingAvatar = styled.span`
height: 32px;
width: 32px;
display: inline-block;
border: 1px solid ${getColor('grey', 10)};
line-height: 32px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
font-size: 15px;
border-radius: 32px;
background-color: ${getColor('white')};
Expand All @@ -29,17 +31,37 @@ type AvatarsProps = Override<
React.HTMLAttributes<HTMLDivElement>,
{
max: number;
maxTitle?: number;
}
>;

const Avatars = ({max, children, ...rest}: AvatarsProps) => {
const Avatars: React.FC<AvatarsProps> = ({max, maxTitle = 10, children, ...rest}) => {
const childrenArray = Children.toArray(children);
const displayedChildren = childrenArray.slice(0, max);
const remainingChildren = childrenArray.slice(max, childrenArray.length + 1);
const remainingChildrenCount = childrenArray.length - max;
const reverseChildren = displayedChildren.reverse();

const remainingUsersTitle = useMemo(() => {
const remainingNames = remainingChildren
.map(child => {
if (!React.isValidElement<AvatarProps>(child)) return;
const {firstName, lastName, username} = child.props;

return `${firstName || ''} ${lastName || ''}`.trim() || username;
})
.slice(0, maxTitle)
.join('\n');

if (remainingChildren.length > maxTitle) {
return remainingNames.concat('\n', '...');
}

return remainingNames;
}, [maxTitle, remainingChildren]);

return (
<AvatarListContainer {...rest}>
<AvatarListContainer title={rest.title || remainingUsersTitle} {...rest}>
{remainingChildrenCount > 0 && <RemainingAvatar>+{remainingChildrenCount}</RemainingAvatar>}
{reverseChildren}
</AvatarListContainer>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import {render, screen} from '../../storybook/test-util';
import {fireEvent, render, screen} from '../../storybook/test-util';
import {Avatar} from './Avatar';
import {Avatars} from './Avatars';

Expand All @@ -24,7 +24,7 @@ test('renders a maximum number of avatars', () => {
);

expect(screen.getByTitle('John Doe')).toBeInTheDocument();
expect(screen.queryByTitle('Leonard Doe')).not.toBeInTheDocument();
expect(screen.queryByText('LD')).not.toBeInTheDocument();
expect(screen.getByText('+1')).toBeInTheDocument();
});

Expand All @@ -39,3 +39,23 @@ test('supports ...rest props', () => {

expect(screen.getByTestId('my_value')).toBeInTheDocument();
});

test('displays remaining users names on plus hover', () => {
const invalidChild = 'I should not be in the title';
render(
<Avatars max={1} maxTitle={1}>
<Avatar username="dSchrute" firstName="Dwight" lastName="Schrute" />
<Avatar username="mscott" firstName=" " lastName=" " />
<Avatar username="kMalone" firstName="Kevin" lastName="Malone" />
{invalidChild}
</Avatars>
);

expect(screen.getByText('DS')).toBeInTheDocument();
expect(screen.getByText('+3')).toBeInTheDocument();
// Kevin Malone should not be visible as it should be part of the +1
expect(screen.queryByText('mscott')).not.toBeInTheDocument();

fireEvent.mouseOver(screen.getByText('+3'));
expect(screen.getByTitle('mscott ...')).toBeInTheDocument();
});
34 changes: 34 additions & 0 deletions front-packages/akeneo-design-system/src/components/Avatar/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {Override} from '../../shared';
import React from 'react';

export type User = {
/**
* Username to use as fallback if the avatar is not provided and the Firstname and Lastname are empty.
*/
username: string;

/**
* Firstname to use as fallback with the Lastname if the avatar is not provided.
*/
firstName: string;

/**
* Lastname to use as fallback with the Firstname if the avatar is not provided.
*/
lastName: string;

/**
* Url of the avatar image.
*/
avatarUrl?: string;
};

export type AvatarProps = Override<
React.HTMLAttributes<HTMLSpanElement>,
User & {
/**
* Size of the avatar.
*/
size?: 'default' | 'big';
}
>;
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const Title = styled.span`
text-overflow: ellipsis;
`;

type SurtitleProps = {label: string};
type SurtitleProps = {label: string; children?: React.ReactNode};

const Surtitle: React.FC<SurtitleProps> = ({label, children, ...rest}) => (
<SurtitleContainer {...rest}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { Meta, Story, ArgsTable, Canvas } from '@storybook/addon-docs';
import { SubNavigationPanel } from './SubNavigationPanel.tsx';
import { SpaceContainer } from '../../../storybook/PreviewGallery';
import { useBooleanState } from '../../../hooks';
import { MoreVerticalIcon } from "../../../icons";
import { Dropdown } from "../../Dropdown/Dropdown";
import { Link } from "../../Link/Link";
import {Meta, Story, ArgsTable, Canvas} from '@storybook/addon-docs';
import {SubNavigationPanel} from './SubNavigationPanel.tsx';
import {SpaceContainer} from '../../../storybook/PreviewGallery';
import {useBooleanState} from '../../../hooks';
import {MoreVerticalIcon} from '../../../icons';
import {Dropdown} from '../../Dropdown/Dropdown';
import {Link} from '../../Link/Link';
import {useState} from 'react';
import {Collapse} from '../../Collapse/Collapse';

<Meta
title="Components/Navigation/SubNavigationPanel"
subcomponents={{
SubNavigationPanel: SubNavigationPanel,
'SubNavigationPanel.Collapsed': SubNavigationPanel.Collapsed,
}}
args={{ children: 'Some content', isOpen: true, closeTitle: 'Close', openTitle: 'Open' }}
args={{children: 'Some content', isOpen: true, closeTitle: 'Close', openTitle: 'Open'}}
/>

# SubNavigationPanel
Expand All @@ -33,9 +35,10 @@ When the panel is collapsed the content is hidden.
<Canvas>
<Story name="Standard">
{args => {
const [isOpen, open, close] = useBooleanState(true);
return (
<SpaceContainer height={200}>
<SubNavigationPanel {...args} />
<SubNavigationPanel {...args} isOpen={isOpen} open={open} close={close} />
</SpaceContainer>
);
}}
Expand All @@ -44,34 +47,44 @@ When the panel is collapsed the content is hidden.

<ArgsTable story="Standard" />

## Panel is collapsed
## Panel with scrollable content

<Canvas>
<Story name="Collapsed">
<Story name="ScrollableContent">
{args => {
const [isOpen, open, close] = useBooleanState(true);
return (
<SpaceContainer height={200}>
<SubNavigationPanel {...args} isOpen={false} />
<SubNavigationPanel {...args} isOpen={isOpen} open={open} close={close}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<br />
Fusce sed quam pharetra, lacinia nisl at, luctus ex.
<br />
Donec pretium est a augue dapibus, at semper ipsum vestibulum.
<br />
Aenean blandit metus a nibh blandit porta.
<br />
Phasellus placerat ligula sit amet vestibulum tristique.
</SubNavigationPanel>
</SpaceContainer>
);
}}
</Story>
</Canvas>


## Panel with collapsed and expanded content
## Panel with collapsed content using Dropdown component

<Canvas>
<Story name="CollapsedExpandedContent">
{args => {
const [isOpen, open, close] = useBooleanState(true);
const [isOpen, open, close] = useBooleanState(false);
const [isDropdownOpen, openDropDown, closeDropDown] = useBooleanState(false);
return (
<SpaceContainer height={200}>
<SubNavigationPanel {...args} isOpen={isOpen} open={open} close={close}>
<SubNavigationPanel.Collapsed>
<Dropdown>
<MoreVerticalIcon title="Collapsed Content" onClick={openDropDown} />
<MoreVerticalIcon title="More" onClick={openDropDown} />
{isDropdownOpen && (
<Dropdown.Overlay onClose={closeDropDown}>
<Dropdown.ItemCollection>
Expand All @@ -81,15 +94,74 @@ When the panel is collapsed the content is hidden.
)}
</Dropdown>
</SubNavigationPanel.Collapsed>
<Link>Expanded Content</Link>
<div>Expanded Content</div>
<div>Expanded Content</div>
<div>Expanded Content</div>
<div>Expanded Content</div>
<div>Expanded Content</div>
<div>Expanded Content</div>
<div>Expanded Content</div>
<div>Expanded Content</div>
Some content
</SubNavigationPanel>
</SpaceContainer>
);
}}
</Story>
</Canvas>

## Panel without padding

<Canvas>
<Story name="ContentWithoutPadding">
{args => {
const [isOpen, open, close] = useBooleanState(true);
const [collapse, setCollapse] = useState(1);
return (
<SpaceContainer height={200}>
<SubNavigationPanel {...args} isOpen={isOpen} open={open} close={close} noPadding>
Some content
</SubNavigationPanel>
</SpaceContainer>
);
}}
</Story>
</Canvas>

## Panel using Collapse components

<Canvas>
<Story name="ContentWithCollapseComponent">
{args => {
const [isOpen, open, close] = useBooleanState(true);
const [collapse, setCollapse] = useState(1);
return (
<SpaceContainer height={200}>
<SubNavigationPanel {...args} isOpen={isOpen} open={open} close={close} noPadding>
<Collapse
label="First Collapse"
collapseButtonLabel="Collapse"
isOpen={collapse === 1}
onCollapse={() => setCollapse(1)}
>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<br />
Fusce sed quam pharetra, lacinia nisl at, luctus ex.
<br />
Donec pretium est a augue dapibus, at semper ipsum vestibulum.
<br />
Aenean blandit metus a nibh blandit porta.
<br />
Phasellus placerat ligula sit amet vestibulum tristique.
</Collapse>
<Collapse
label="Second Collapse"
collapseButtonLabel="Collapse"
isOpen={collapse === 2}
onCollapse={() => setCollapse(2)}
>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<br />
Fusce sed quam pharetra, lacinia nisl at, luctus ex.
<br />
Donec pretium est a augue dapibus, at semper ipsum vestibulum.
<br />
Aenean blandit metus a nibh blandit porta.
<br />
Phasellus placerat ligula sit amet vestibulum tristique.
</Collapse>
</SubNavigationPanel>
</SpaceContainer>
);
Expand Down
Loading
Loading