Skip to content

Commit

Permalink
Merge pull request #20386 from akeneo/dsm-update-2024-01-10
Browse files Browse the repository at this point in the history
DSM Update
  • Loading branch information
tseho committed Jan 10, 2024
2 parents 89cbc95 + 5e8d863 commit 164a43d
Show file tree
Hide file tree
Showing 12 changed files with 245 additions and 135 deletions.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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

0 comments on commit 164a43d

Please sign in to comment.