diff --git a/front-packages/akeneo-design-system/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-navigation-sub-navigation-panel-content-with-collapse-component-correctly-1-snap.png b/front-packages/akeneo-design-system/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-navigation-sub-navigation-panel-content-with-collapse-component-correctly-1-snap.png new file mode 100644 index 000000000000..60a7d2dad9e9 Binary files /dev/null and b/front-packages/akeneo-design-system/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-navigation-sub-navigation-panel-content-with-collapse-component-correctly-1-snap.png differ diff --git a/front-packages/akeneo-design-system/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-navigation-sub-navigation-panel-content-without-padding-correctly-1-snap.png b/front-packages/akeneo-design-system/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-navigation-sub-navigation-panel-content-without-padding-correctly-1-snap.png new file mode 100644 index 000000000000..bc3fbfe07036 Binary files /dev/null and b/front-packages/akeneo-design-system/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-navigation-sub-navigation-panel-content-without-padding-correctly-1-snap.png differ diff --git a/front-packages/akeneo-design-system/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-navigation-sub-navigation-panel-scrollable-content-correctly-1-snap.png b/front-packages/akeneo-design-system/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-navigation-sub-navigation-panel-scrollable-content-correctly-1-snap.png new file mode 100644 index 000000000000..f46d0fc93818 Binary files /dev/null and b/front-packages/akeneo-design-system/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-navigation-sub-navigation-panel-scrollable-content-correctly-1-snap.png differ diff --git a/front-packages/akeneo-design-system/src/components/Avatar/Avatar.stories.mdx b/front-packages/akeneo-design-system/src/components/Avatar/Avatar.stories.mdx index 0ec0bfd22941..d84c11bcb9e3 100644 --- a/front-packages/akeneo-design-system/src/components/Avatar/Avatar.stories.mdx +++ b/front-packages/akeneo-design-system/src/components/Avatar/Avatar.stories.mdx @@ -92,7 +92,7 @@ You can use a dedicated component to display avatar list. After a defined maximu <> ` ${({size}) => @@ -31,43 +31,13 @@ const AvatarContainer = styled.span` cursor: ${({onClick}) => (onClick ? 'pointer' : 'default')}; `; -type AvatarProps = Override< - React.HTMLAttributes, - { - /** - * 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((s, l) => s + l.charCodeAt(0), 0); diff --git a/front-packages/akeneo-design-system/src/components/Avatar/Avatars.tsx b/front-packages/akeneo-design-system/src/components/Avatar/Avatars.tsx index dbd5a1178d82..375058105233 100644 --- a/front-packages/akeneo-design-system/src/components/Avatar/Avatars.tsx +++ b/front-packages/akeneo-design-system/src/components/Avatar/Avatars.tsx @@ -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` display: flex; @@ -16,10 +17,11 @@ const AvatarListContainer = styled.div` 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')}; @@ -29,17 +31,37 @@ type AvatarsProps = Override< React.HTMLAttributes, { max: number; + maxTitle?: number; } >; -const Avatars = ({max, children, ...rest}: AvatarsProps) => { +const Avatars: React.FC = ({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(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 ( - + {remainingChildrenCount > 0 && +{remainingChildrenCount}} {reverseChildren} diff --git a/front-packages/akeneo-design-system/src/components/Avatar/Avatars.unit.tsx b/front-packages/akeneo-design-system/src/components/Avatar/Avatars.unit.tsx index 2f8ac9ea39b4..f1c1f5c425b4 100644 --- a/front-packages/akeneo-design-system/src/components/Avatar/Avatars.unit.tsx +++ b/front-packages/akeneo-design-system/src/components/Avatar/Avatars.unit.tsx @@ -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'; @@ -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(); }); @@ -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( + + + + + {invalidChild} + + ); + + 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(); +}); diff --git a/front-packages/akeneo-design-system/src/components/Avatar/types.ts b/front-packages/akeneo-design-system/src/components/Avatar/types.ts new file mode 100644 index 000000000000..c48a0a865d26 --- /dev/null +++ b/front-packages/akeneo-design-system/src/components/Avatar/types.ts @@ -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, + User & { + /** + * Size of the avatar. + */ + size?: 'default' | 'big'; + } +>; diff --git a/front-packages/akeneo-design-system/src/components/Dropdown/Surtitle/Surtitle.tsx b/front-packages/akeneo-design-system/src/components/Dropdown/Surtitle/Surtitle.tsx index 5e300b2a8ef7..029d0bbea874 100644 --- a/front-packages/akeneo-design-system/src/components/Dropdown/Surtitle/Surtitle.tsx +++ b/front-packages/akeneo-design-system/src/components/Dropdown/Surtitle/Surtitle.tsx @@ -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 = ({label, children, ...rest}) => ( diff --git a/front-packages/akeneo-design-system/src/components/Navigation/SubNavigationPanel/SubNavigationPanel.stories.mdx b/front-packages/akeneo-design-system/src/components/Navigation/SubNavigationPanel/SubNavigationPanel.stories.mdx index c34f1e7f18ed..a7eaaf9f0623 100644 --- a/front-packages/akeneo-design-system/src/components/Navigation/SubNavigationPanel/SubNavigationPanel.stories.mdx +++ b/front-packages/akeneo-design-system/src/components/Navigation/SubNavigationPanel/SubNavigationPanel.stories.mdx @@ -1,10 +1,12 @@ -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'; # SubNavigationPanel @@ -33,9 +35,10 @@ When the panel is collapsed the content is hidden. {args => { + const [isOpen, open, close] = useBooleanState(true); return ( - + ); }} @@ -44,34 +47,44 @@ When the panel is collapsed the content is hidden. -## Panel is collapsed +## Panel with scrollable content - + {args => { + const [isOpen, open, close] = useBooleanState(true); return ( - + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. +
+ Fusce sed quam pharetra, lacinia nisl at, luctus ex. +
+ Donec pretium est a augue dapibus, at semper ipsum vestibulum. +
+ Aenean blandit metus a nibh blandit porta. +
+ Phasellus placerat ligula sit amet vestibulum tristique. +
); }}
- -## Panel with collapsed and expanded content +## Panel with collapsed content using Dropdown component {args => { - const [isOpen, open, close] = useBooleanState(true); + const [isOpen, open, close] = useBooleanState(false); const [isDropdownOpen, openDropDown, closeDropDown] = useBooleanState(false); return ( - + {isDropdownOpen && ( @@ -81,15 +94,74 @@ When the panel is collapsed the content is hidden. )} - Expanded Content -
Expanded Content
-
Expanded Content
-
Expanded Content
-
Expanded Content
-
Expanded Content
-
Expanded Content
-
Expanded Content
-
Expanded Content
+ Some content +
+
+ ); + }} +
+
+ +## Panel without padding + + + + {args => { + const [isOpen, open, close] = useBooleanState(true); + const [collapse, setCollapse] = useState(1); + return ( + + + Some content + + + ); + }} + + + +## Panel using Collapse components + + + + {args => { + const [isOpen, open, close] = useBooleanState(true); + const [collapse, setCollapse] = useState(1); + return ( + + + setCollapse(1)} + > + Lorem ipsum dolor sit amet, consectetur adipiscing elit. +
+ Fusce sed quam pharetra, lacinia nisl at, luctus ex. +
+ Donec pretium est a augue dapibus, at semper ipsum vestibulum. +
+ Aenean blandit metus a nibh blandit porta. +
+ Phasellus placerat ligula sit amet vestibulum tristique. +
+ setCollapse(2)} + > + Lorem ipsum dolor sit amet, consectetur adipiscing elit. +
+ Fusce sed quam pharetra, lacinia nisl at, luctus ex. +
+ Donec pretium est a augue dapibus, at semper ipsum vestibulum. +
+ Aenean blandit metus a nibh blandit porta. +
+ Phasellus placerat ligula sit amet vestibulum tristique. +
); diff --git a/front-packages/akeneo-design-system/src/components/Navigation/SubNavigationPanel/SubNavigationPanel.tsx b/front-packages/akeneo-design-system/src/components/Navigation/SubNavigationPanel/SubNavigationPanel.tsx index 3c4c39d49cbd..c435990e9dfa 100644 --- a/front-packages/akeneo-design-system/src/components/Navigation/SubNavigationPanel/SubNavigationPanel.tsx +++ b/front-packages/akeneo-design-system/src/components/Navigation/SubNavigationPanel/SubNavigationPanel.tsx @@ -1,54 +1,42 @@ -import React, {ForwardRefExoticComponent, PropsWithoutRef, ReactNode, RefAttributes} from 'react'; -import styled, {css} from 'styled-components'; +import React, {ForwardRefExoticComponent, PropsWithoutRef, RefAttributes, useLayoutEffect} from 'react'; +import styled from 'styled-components'; import {PanelCloseIcon, PanelOpenIcon} from '../../../icons'; import {AkeneoThemedProps, getColor} from '../../../theme'; -const collapsableStyles = css<{isOpen: boolean} & AkeneoThemedProps>` - opacity: ${({isOpen}) => (isOpen ? 1 : 0)}; - transition: opacity 0.3s; - transition-delay: 0.3s; - - position: relative; - z-index: 1; - ${({isOpen}) => - !isOpen && - css` - position: absolute; - z-index: 0; - `} -`; -const Panel = styled.div<{isOpen: boolean} & AkeneoThemedProps>` +const Container = styled.div<{isOpen: boolean} & AkeneoThemedProps>` + background-color: ${getColor('grey', 20)}; + border-right: 1px solid ${getColor('grey', 80)}; + box-sizing: border-box; display: flex; flex-direction: column; - height: calc(100% - 54px); - width: 100%; - position: absolute; - overflow-y: ${({isOpen}) => (isOpen ? 'auto' : 'hidden')}; - overflow-x: hidden; + height: 100%; + transition: width 0.3s linear; + width: ${({isOpen}) => (isOpen ? '280px' : '40px')}; `; -const PanelContent = styled.div<{isOpen: boolean} & AkeneoThemedProps>` - box-sizing: border-box; +const Content = styled.div<{isOpen: boolean; noPadding: boolean}>` + display: flex; + flex-direction: column; flex-grow: 1; - width: 100%; - padding: ${({isOpen}) => (isOpen ? '30px' : '10px 5px')}; - ${collapsableStyles} + overflow-x: hidden; + overflow-y: auto; + opacity: ${({isOpen}) => (isOpen ? '1' : '0')}; + transition: ${({isOpen}) => (isOpen ? 'opacity 300ms linear 300ms' : 'none')}; + padding: ${({noPadding}) => (noPadding ? '0' : '30px')}; `; -const ToggleButton = styled.button<{isOpen: boolean} & AkeneoThemedProps>` +const ToggleButton = styled.button` align-items: center; background: none; border: none; border-top: 1px solid ${getColor('grey', 80)}; + box-sizing: border-box; cursor: pointer; + display: flex; + flex: 0 0 auto; height: 54px; - margin: ${({isOpen}) => (isOpen ? '0 20px' : '0')}; - padding: ${({isOpen}) => (isOpen ? '0' : '0 12.5px')}; - transition: margin 0.3s ease-in-out, padding 0.3s ease-in-out; - text-align: left; - position: absolute; - bottom: 0; - width: ${({isOpen}) => (isOpen ? '240px' : '40px')}; + padding: 0; + padding-left: 12.5px; svg { color: ${getColor('grey', 100)}; @@ -56,30 +44,10 @@ const ToggleButton = styled.button<{isOpen: boolean} & AkeneoThemedProps>` } `; -const Container = styled.div<{isOpen: boolean} & AkeneoThemedProps>` - width: ${({isOpen}) => (isOpen ? '280px' : '40px')}; - transition: width 0.3s linear; - position: relative; - order: -10; - background-color: ${getColor('grey', 20)}; - border-right: 1px solid ${getColor('grey', 80)}; - flex-shrink: 0; - height: 100%; - z-index: 802; - overflow: hidden; -`; - -const Collapsed = styled.div<{isOpen: boolean} & AkeneoThemedProps>` - flex-grow: 1; +const Collapsed = styled.div` padding: 10px 5px; - ${collapsableStyles} `; -Collapsed.displayName = 'Collapsed'; -Collapsed.defaultProps = { - isOpen: false, -}; - type SubNavigationPanelProps = { /** * The content of the panel. @@ -110,12 +78,18 @@ type SubNavigationPanelProps = { * Opening title to display for the ToggleButton */ openTitle?: string; + + /** + * Whether or not the panel should have padding. + */ + noPadding?: boolean; }; type SubNavigationPanelCompoundType = ForwardRefExoticComponent< PropsWithoutRef & RefAttributes > & { Collapsed?: any; + Section?: any; }; /** @@ -123,25 +97,40 @@ type SubNavigationPanelCompoundType = ForwardRefExoticComponent< */ const SubNavigationPanel: SubNavigationPanelCompoundType = React.forwardRef( ( - {children, isOpen: isOpen = true, open, close, closeTitle = '', openTitle = '', ...rest}: SubNavigationPanelProps, + { + children, + isOpen = true, + open, + close, + closeTitle = '', + openTitle = '', + noPadding = false, + ...rest + }: SubNavigationPanelProps, forwardedRef: React.Ref ) => { - const contentChildren: ReactNode[] = []; - let collapsedContent: ReactNode | null = null; + const collapsedElements: React.ReactNode[] = []; + const contentElements: React.ReactNode[] = []; + React.Children.forEach(children, child => { if (React.isValidElement(child) && child.type === Collapsed) { - collapsedContent = React.cloneElement(child, {isOpen: !isOpen}); - return; + collapsedElements.push(child); + } else { + contentElements.push(child); } - contentChildren.push(child); }); + const [isOpenTransition, setIsOpenTransition] = React.useState(isOpen); + useLayoutEffect(() => { + setIsOpenTransition(isOpen); + }, [isOpen]); + return ( - - {collapsedContent} - {isOpen && contentChildren} - + {!isOpen && collapsedElements} + + {isOpen && contentElements} + (isOpen ? close() : open())} @@ -155,6 +144,9 @@ const SubNavigationPanel: SubNavigationPanelCompoundType = React.forwardRef { ); expect(queryByText('SubNavigationPanel content')).not.toBeInTheDocument(); - expect(getByText('Collapsed content')).toBeVisible(); + expect(getByText('Collapsed content')).toBeInTheDocument(); }); test('it hides collapsed content', () => { @@ -89,6 +89,6 @@ test('it hides collapsed content', () => { ); - expect(queryByText('SubNavigationPanel content')).toBeInTheDocument(); - expect(getByText('Collapsed content')).not.toBeVisible(); + expect(getByText('SubNavigationPanel content')).toBeInTheDocument(); + expect(queryByText('Collapsed content')).not.toBeInTheDocument(); });