diff --git a/plugins/woocommerce-admin/packages/components/CHANGELOG.md b/plugins/woocommerce-admin/packages/components/CHANGELOG.md index 3a6718837f8..65ffc519632 100644 --- a/plugins/woocommerce-admin/packages/components/CHANGELOG.md +++ b/plugins/woocommerce-admin/packages/components/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased +- Add new (experimental) collapsible list item to collapse list items. #6869 - SelectControl: fix display of multiple selections without inline tags. #6862 - Add new (experimental) list, and add depreciation notice for the current list. #6787 - Force `` form elements id to be unique. #6871 diff --git a/plugins/woocommerce-admin/packages/components/src/index.js b/plugins/woocommerce-admin/packages/components/src/index.js index 0cfb0c4bd78..e9b28d41ab7 100644 --- a/plugins/woocommerce-admin/packages/components/src/index.js +++ b/plugins/woocommerce-admin/packages/components/src/index.js @@ -29,6 +29,8 @@ export { default as List, ExperimentalList as __experimentalList, ExperimentalListItem as __experimentalListItem, + ExperimentalListItemCollapse as __experimentalListItemCollapse, + ExperimentalCollapsibleList as __experimentalCollapsibleList, } from './list'; export { default as MenuItem } from './ellipsis-menu/menu-item'; export { default as MenuTitle } from './ellipsis-menu/menu-title'; diff --git a/plugins/woocommerce-admin/packages/components/src/list/collapsible-list/index.tsx b/plugins/woocommerce-admin/packages/components/src/list/collapsible-list/index.tsx new file mode 100644 index 00000000000..1468e8ae243 --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/list/collapsible-list/index.tsx @@ -0,0 +1,127 @@ +/** + * External dependencies + */ +import { Dashicon } from '@wordpress/components'; +import { useState, useCallback, Children } from '@wordpress/element'; +import { Transition } from 'react-transition-group'; + +/** + * Internal dependencies + */ +import { ExperimentalListItem } from '../experimental-list-item'; +import { ListProps, ExperimentalList } from '../experimental-list'; + +type CollapsibleListProps = { + collapseLabel: string; + expandLabel: string; + collapsed?: boolean; + show?: number; + onCollapse?: () => void; + onExpand?: () => void; +} & ListProps; + +const defaultStyle = { + transition: `max-height 500ms ease-in-out`, + maxHeight: 0, + overflow: 'hidden', +}; + +function getContainerHeight( collapseContainer: HTMLDivElement | null ) { + let containerHeight = 0; + if ( collapseContainer ) { + for ( const child of collapseContainer.children ) { + containerHeight += child.clientHeight; + } + } + return containerHeight; +} + +export const ExperimentalCollapsibleList: React.FC< CollapsibleListProps > = ( { + children, + collapsed = true, + collapseLabel, + expandLabel, + show = 0, + onCollapse, + onExpand, + ...listProps +} ): JSX.Element => { + const [ isCollapsed, setCollapsed ] = useState( collapsed ); + const [ containerHeight, setContainerHeight ] = useState( 0 ); + const collapseContainerRef = useCallback( + ( containerElement: HTMLDivElement ) => { + if ( containerElement ) { + setContainerHeight( getContainerHeight( containerElement ) ); + } + }, + [ children ] + ); + + const triggerCallbacks = ( newCollapseValue: boolean ) => { + if ( onCollapse && newCollapseValue ) { + onCollapse(); + } + if ( onExpand && ! newCollapseValue ) { + onExpand(); + } + }; + + const clickHandler = useCallback( () => { + setCollapsed( ! isCollapsed ); + triggerCallbacks( ! isCollapsed ); + }, [ isCollapsed ] ); + + let shownChildren: React.ReactNode[] = []; + let hiddenChildren = Children.toArray( children ); + if ( show > 0 ) { + shownChildren = hiddenChildren.slice( 0, show ); + hiddenChildren = hiddenChildren.slice( show ); + } + + const transitionStyles = { + entered: { maxHeight: containerHeight }, + entering: { maxHeight: containerHeight }, + exiting: { maxHeight: 0 }, + exited: { maxHeight: 0 }, + }; + + return ( + + { shownChildren } + + { ( state: 'entering' | 'entered' | 'exiting' | 'exited' ) => ( +
+ { hiddenChildren } +
+ ) } +
+ { hiddenChildren.length > 0 ? ( + +

{ isCollapsed ? expandLabel : collapseLabel }

+ + +
+ ) : null } +
+ ); +}; diff --git a/plugins/woocommerce-admin/packages/components/src/list/collapsible-list/style.scss b/plugins/woocommerce-admin/packages/components/src/list/collapsible-list/style.scss new file mode 100644 index 00000000000..cf463cb594a --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/list/collapsible-list/style.scss @@ -0,0 +1,11 @@ +.woocommerce-list__item.list-item-collapse { + justify-content: space-between; + + p { + margin: 0; + } + + .list-item-collapse__icon-container { + justify-content: flex-end; + } +} diff --git a/plugins/woocommerce-admin/packages/components/src/list/experimental-list.tsx b/plugins/woocommerce-admin/packages/components/src/list/experimental-list.tsx index 0b31c8d937f..62512b80667 100644 --- a/plugins/woocommerce-admin/packages/components/src/list/experimental-list.tsx +++ b/plugins/woocommerce-admin/packages/components/src/list/experimental-list.tsx @@ -11,7 +11,7 @@ import type { ListAnimation } from './experimental-list-item'; type ListType = 'ol' | 'ul'; -type ListProps = { +export type ListProps = { listType?: ListType; animation?: ListAnimation; } & React.HTMLAttributes< HTMLElement >; diff --git a/plugins/woocommerce-admin/packages/components/src/list/index.js b/plugins/woocommerce-admin/packages/components/src/list/index.js index 023ab7e00a9..3aa7ac69701 100644 --- a/plugins/woocommerce-admin/packages/components/src/list/index.js +++ b/plugins/woocommerce-admin/packages/components/src/list/index.js @@ -115,3 +115,4 @@ export default List; export { ExperimentalListItem } from './experimental-list-item'; export { ExperimentalList } from './experimental-list'; +export { ExperimentalCollapsibleList } from './collapsible-list'; diff --git a/plugins/woocommerce-admin/packages/components/src/list/stories/index.js b/plugins/woocommerce-admin/packages/components/src/list/stories/index.js index 0204265585b..4aee54a5236 100644 --- a/plugins/woocommerce-admin/packages/components/src/list/stories/index.js +++ b/plugins/woocommerce-admin/packages/components/src/list/stories/index.js @@ -7,7 +7,11 @@ import { withConsole } from '@storybook/addon-console'; /** * Internal dependencies */ -import List, { ExperimentalList, ExperimentalListItem } from '../'; +import List, { + ExperimentalList, + ExperimentalListItem, + ExperimentalCollapsibleList, +} from '../'; import './style.scss'; function logItemClick( event ) { @@ -161,4 +165,47 @@ export const ExperimentalListExample = () => { ); }; -ExperimentalList.storyName = 'ExperimentalList / ExperimentalListItem.'; +ExperimentalListExample.storyName = 'ExperimentalList / ExperimentalListItem.'; + +export const ExperimentalCollapsibleListExample = () => { + return ( + { + // eslint-disable-next-line no-console + console.log( 'collapsed' ); + } } + onExpand={ () => { + // eslint-disable-next-line no-console + console.log( 'expanded' ); + } } + > + {} }> +
Any markup can go here.
+
+ {} }> +
Any markup can go here.
+
+ {} }> +
+ Any markup can go here. +
+ Bigger task item +
+ Another line +
+
+ {} }> +
Any markup can go here.
+
+ {} }> +
Any markup can go here.
+
+
+ ); +}; + +ExperimentalCollapsibleListExample.storyName = + 'ExperimentalList with ExperimentalCollapsibleListItem.'; diff --git a/plugins/woocommerce-admin/packages/components/src/list/test/index.js b/plugins/woocommerce-admin/packages/components/src/list/test/index.js index 679a06b9b33..85ebd96faeb 100644 --- a/plugins/woocommerce-admin/packages/components/src/list/test/index.js +++ b/plugins/woocommerce-admin/packages/components/src/list/test/index.js @@ -7,13 +7,21 @@ jest.mock( '../list-item', () => ( { /** * External dependencies */ -import { render, screen } from '@testing-library/react'; +import { + render, + screen, + waitForElementToBeRemoved, +} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; /** * Internal dependencies */ -import List, { ExperimentalList, ExperimentalListItem } from '../index'; +import List, { + ExperimentalList, + ExperimentalListItem, + ExperimentalCollapsibleList, +} from '../index'; import { handleKeyDown } from '../list-item'; describe( 'List', () => { @@ -145,6 +153,124 @@ describe( 'List', () => { expect( item ).toHaveAttribute( 'tabindex', '0' ); } ); } ); + + describe( 'ExperimentalListItemCollapse', () => { + it( 'should not render its children intially, but an extra list footer with show text', () => { + const { container } = render( + +
Test
+
+ ); + + expect( container ).not.toHaveTextContent( 'Test' ); + expect( container ).toHaveTextContent( 'Show more items' ); + } ); + + it( 'should render list items when footer is clicked and trigger onExpand', () => { + const onExpand = jest.fn(); + const onCollapse = jest.fn(); + const { container } = render( + +
Test
+
Test 2
+
+ ); + + const listItem = container.querySelector( + '.list-item-collapse' + ); + + userEvent.click( listItem ); + expect( container ).toHaveTextContent( 'Test' ); + expect( container ).toHaveTextContent( 'Test 2' ); + expect( container ).not.toHaveTextContent( 'Show more items' ); + expect( container ).toHaveTextContent( 'Show less' ); + expect( onExpand ).toHaveBeenCalled(); + expect( onCollapse ).not.toHaveBeenCalled(); + } ); + + it( 'should render minimum children if minChildrenToShow is set and show the rest on expand', () => { + const onExpand = jest.fn(); + const onCollapse = jest.fn(); + const { container } = render( + +
Test
+
Test 2
+
Test 3
+
Test 4
+
+ ); + + expect( container ).toHaveTextContent( 'Test' ); + expect( container ).toHaveTextContent( 'Test 2' ); + expect( container ).not.toHaveTextContent( 'Test 3' ); + expect( container ).not.toHaveTextContent( 'Test 4' ); + const listItem = container.querySelector( + '.list-item-collapse' + ); + + userEvent.click( listItem ); + expect( container ).toHaveTextContent( 'Test' ); + expect( container ).toHaveTextContent( 'Test 2' ); + expect( container ).toHaveTextContent( 'Test 3' ); + expect( container ).toHaveTextContent( 'Test 4' ); + expect( container ).not.toHaveTextContent( 'Show more items' ); + expect( container ).toHaveTextContent( 'Show less' ); + expect( onExpand ).toHaveBeenCalled(); + expect( onCollapse ).not.toHaveBeenCalled(); + } ); + + it( 'should correctly toggle the list', async () => { + const onExpand = jest.fn(); + const onCollapse = jest.fn(); + const { container } = render( + +
Test
+
Test 2
+
+ ); + + let listItem = container.querySelector( '.list-item-collapse' ); + + userEvent.click( listItem ); + expect( container ).toHaveTextContent( 'Test' ); + expect( container ).toHaveTextContent( 'Test 2' ); + expect( container ).not.toHaveTextContent( 'Show more items' ); + expect( container ).toHaveTextContent( 'Show less' ); + + listItem = container.querySelector( '.list-item-collapse' ); + + userEvent.click( listItem ); + await waitForElementToBeRemoved( + container.querySelector( '#test' ) + ); + expect( container ).not.toHaveTextContent( 'Test' ); + expect( container ).not.toHaveTextContent( 'Test 2' ); + expect( container ).toHaveTextContent( 'Show more items' ); + expect( container ).not.toHaveTextContent( 'Show less' ); + expect( onExpand ).toHaveBeenCalledTimes( 1 ); + expect( onCollapse ).toHaveBeenCalledTimes( 1 ); + } ); + } ); } ); describe( 'Legacy List', () => { diff --git a/plugins/woocommerce-admin/packages/components/src/style.scss b/plugins/woocommerce-admin/packages/components/src/style.scss index af7df1e4600..08628d54d5a 100644 --- a/plugins/woocommerce-admin/packages/components/src/style.scss +++ b/plugins/woocommerce-admin/packages/components/src/style.scss @@ -18,6 +18,7 @@ @import 'gravatar/style.scss'; @import 'image-upload/style.scss'; @import 'list/style.scss'; +@import 'list/collapsible-list/style.scss'; @import 'order-status/style.scss'; @import 'pagination/style.scss'; @import 'pill/style.scss';