From 99356920559322d2a3e43083f1dfdf6664111714 Mon Sep 17 00:00:00 2001 From: Pavel Klibani Date: Mon, 2 Dec 2024 18:13:56 +0100 Subject: [PATCH] Refactor(web-react): Collapse API prop changed - prop `hideOnCollapse`` changed to `isDisposable` - Solves DS-832 --- packages/web-react/DEPRECATIONS.md | 13 +- .../src/components/Collapse/README.md | 27 ++-- .../Collapse/UncontrolledCollapse.tsx | 15 ++- .../Collapse/__tests__/Collapse.test.tsx | 115 ++++++++++++++++++ .../__tests__/UncontrolledCollapse.test.tsx | 28 +++++ .../Collapse/demo/CollapseUncontrolled.tsx | 27 ++-- .../stories/UncontrolledCollapse.stories.tsx | 66 +++++++--- packages/web-react/src/types/collapse.ts | 4 +- .../Resources/components/Collapse/README.md | 12 +- 9 files changed, 266 insertions(+), 41 deletions(-) create mode 100644 packages/web-react/src/components/Collapse/__tests__/Collapse.test.tsx diff --git a/packages/web-react/DEPRECATIONS.md b/packages/web-react/DEPRECATIONS.md index c08160a422..8d04729678 100644 --- a/packages/web-react/DEPRECATIONS.md +++ b/packages/web-react/DEPRECATIONS.md @@ -6,8 +6,17 @@ This document lists all deprecations that will be removed in the next major vers ## Deprecations -Nothing here right now! 🎉 - 👉 [What are deprecations?][readme-deprecations] +### UncontrolledCollapse `isDisposable` + +The `hideOnCollapse` prop was removed, please use `isDisposable` instead. + +#### Migration Guide + +We are providing a [codemod][codemod-collapse] to assist with this change. + +- `` → `` + +[codemod-collapse]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/codemods/src/transforms/v4/web-react/README.md#v4web-reactcollapse-isdisposable-prop--uncontrolledcollapse-hideoncollapse-to-isdisposable-prop-change [readme-deprecations]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/README.md#deprecations diff --git a/packages/web-react/src/components/Collapse/README.md b/packages/web-react/src/components/Collapse/README.md index c92ee2c1c3..75b21e7cd1 100644 --- a/packages/web-react/src/components/Collapse/README.md +++ b/packages/web-react/src/components/Collapse/README.md @@ -127,19 +127,28 @@ import { Button, UncontrolledCollapse } from '@lmc-eu/spirit-web-react/component ## API -| Name | Type | Default | Required | Description | -| ------------------------- | -------------------------------------------- | ------- | -------- | ------------------------------------------- | -| `collapsibleToBreakpoint` | [`mobile` \| `tablet` \| `desktop`] | — | ✕ | Handle for responsive breakpoint | -| `elementType` | [`span` \| `div`] | `div` | ✕ | Type of element used as wrapper and content | -| `id` | `string` | — | ✓ | Component id | -| `isOpen` | `bool` | — | ✕ | Is open on initialization | -| `hideOnCollapse` | `bool` | — | ✕ | Hides button when content is displayed | -| `renderTrigger` | `(render: CollapseRenderProps) => ReactNode` | — | ✕ | Properties for trigger render | +| Name | Type | Default | Required | Description | +| ------------------------- | -------------------------------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------- | +| `collapsibleToBreakpoint` | [`mobile` \| `tablet` \| `desktop`] | — | ✕ | Handle for responsive breakpoint | +| `elementType` | [`span` \| `div`] | `div` | ✕ | Type of element used as wrapper and content | +| `hideOnCollapse` | `bool` | `false` | ✕ | [**DEPRECATED**][readme-deprecations] in favor of `isDisposable`; Hides button when content is displayed | +| `id` | `string` | — | ✓ | Component id | +| `isDisposable` | `bool` | `false` | ✕ | Hides trigger when content is displayed | +| `isOpen` | `bool` | `false` | ✕ | Is open on initialization | +| `renderTrigger` | `(render: CollapseRenderProps) => ReactNode` | — | ✕ | Properties for trigger render | On top of the API options, the components accept [additional attributes][readme-additional-attributes]. If you need more control over the styling of a component, you can use [style props][readme-style-props] and [escape hatches][readme-escape-hatches]. +### ⚠️ DEPRECATION NOTICE + +`hideOnCollapse` property will be replaced in the next major version. Please use `isDisposable` instead. + +We are providing a [codemod][codemod-collapse] to assist with this change. + +[What are deprecations?][readme-deprecations] + ## Render Toggle API | Name | Type | Default | Required | Description | @@ -149,6 +158,8 @@ and [escape hatches][readme-escape-hatches]. | `aria-expanded` | `Booleanish` | — | ✕ | Trigger aria expanded | | `aria-controls` | `string` | — | ✕ | Trigger aria controls | +[codemod-collapse]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/codemods/src/transforms/v4/web-react/README.md#v4web-reactcollapse-isdisposable-prop--uncontrolledcollapse-hideoncollapse-to-isdisposable-prop-change [readme-additional-attributes]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/README.md#additional-attributes +[readme-deprecations]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/README.md#deprecations [readme-escape-hatches]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/README.md#escape-hatches [readme-style-props]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/README.md#style-props diff --git a/packages/web-react/src/components/Collapse/UncontrolledCollapse.tsx b/packages/web-react/src/components/Collapse/UncontrolledCollapse.tsx index 5e143f69d7..3042e6cf35 100644 --- a/packages/web-react/src/components/Collapse/UncontrolledCollapse.tsx +++ b/packages/web-react/src/components/Collapse/UncontrolledCollapse.tsx @@ -12,12 +12,21 @@ const defaultProps = { const UncontrolledCollapse = (props: SpiritUncontrolledCollapseProps) => { const propsWithDefaults = { ...defaultProps, ...props }; - const { children, hideOnCollapse, renderTrigger, ...restProps } = propsWithDefaults; + const { + children, + /** @deprecated "hideOnCollapse" property will be replaced in the next major version. Please use "isDisposable" instead. */ + hideOnCollapse, + isDisposable, + renderTrigger, + ...restProps + } = propsWithDefaults; const { isOpen, toggleHandler } = useCollapse(restProps.isOpen); const { ariaProps } = useCollapseAriaProps({ ...restProps, isOpen }); + const isDisposed = hideOnCollapse || isDisposable; + const triggerRenderHandler = () => { - const showTrigger = hideOnCollapse ? !(hideOnCollapse && isOpen) : true; + const showTrigger = isDisposed ? !(isDisposed && isOpen) : true; return renderTrigger && showTrigger ? renderTrigger({ @@ -31,7 +40,7 @@ const UncontrolledCollapse = (props: SpiritUncontrolledCollapseProps) => { return ( <> {triggerRenderHandler()} - {hideOnCollapse && isOpen ? ( + {isDisposed && isOpen ? ( children ) : ( diff --git a/packages/web-react/src/components/Collapse/__tests__/Collapse.test.tsx b/packages/web-react/src/components/Collapse/__tests__/Collapse.test.tsx new file mode 100644 index 0000000000..253202df67 --- /dev/null +++ b/packages/web-react/src/components/Collapse/__tests__/Collapse.test.tsx @@ -0,0 +1,115 @@ +import '@testing-library/jest-dom'; +import { act, fireEvent, render, screen } from '@testing-library/react'; +import React, { useState } from 'react'; +import { classNamePrefixProviderTest } from '../../../../tests/providerTests/classNamePrefixProviderTest'; +import { restPropsTest } from '../../../../tests/providerTests/restPropsTest'; +import { stylePropsTest } from '../../../../tests/providerTests/stylePropsTest'; +import { Button } from '../../Button'; +import Collapse from '../Collapse'; + +// @TODO: Missing test for collapsibleToBreakpoint + +describe('Collapse', () => { + classNamePrefixProviderTest(Collapse, 'Collapse'); + + stylePropsTest(Collapse); + + restPropsTest(Collapse, 'div'); + + it('should render text children', () => { + render( + + Hello World + , + ); + + const element = screen.getByTestId('test'); + expect(element).toHaveTextContent('Hello World'); + expect(element).toHaveClass('is-open'); + }); + + it('should open collapse', () => { + const RenderCollapse = () => { + const [isOpen, setIsOpen] = useState(false); + + return ( + <> + + + Hello World + + + ); + }; + + render(); + + const toggleButton = screen.getByTestId('test-button'); + const collapseElement = screen.getByTestId('test'); + + expect(collapseElement).not.toHaveClass('is-open'); + + fireEvent.click(toggleButton); + + expect(collapseElement).toHaveClass('is-open'); + }); + + it('should have correct html element', () => { + render( + + Hello World + , + ); + + const element = screen.getByTestId('test'); + + expect(element.tagName).toBe('SECTION'); + }); + + it('should respect transitionDuration prop', async () => { + jest.useFakeTimers(); + + const RenderCollapse = ({ duration }: { duration: number }) => { + const [isOpen, setIsOpen] = useState(false); + + return ( + <> + + + Hello World + + + ); + }; + + const transitionDuration = 250; + + render(); + + const toggleButton = screen.getByTestId('test-button'); + const collapseElement = screen.getByTestId('test'); + + expect(collapseElement).not.toHaveClass('is-open'); + + fireEvent.click(toggleButton); + + act(() => { + jest.advanceTimersByTime(transitionDuration - 1); + }); + + expect(collapseElement).toHaveClass('is-transitioning'); + + act(() => { + jest.advanceTimersByTime(1); + }); + + expect(collapseElement).not.toHaveClass('is-transitioning'); + expect(collapseElement).toHaveClass('is-open'); + + jest.useRealTimers(); + }); +}); diff --git a/packages/web-react/src/components/Collapse/__tests__/UncontrolledCollapse.test.tsx b/packages/web-react/src/components/Collapse/__tests__/UncontrolledCollapse.test.tsx index c181f4529f..210f5f6c98 100644 --- a/packages/web-react/src/components/Collapse/__tests__/UncontrolledCollapse.test.tsx +++ b/packages/web-react/src/components/Collapse/__tests__/UncontrolledCollapse.test.tsx @@ -4,6 +4,7 @@ import React from 'react'; import { classNamePrefixProviderTest } from '../../../../tests/providerTests/classNamePrefixProviderTest'; import { restPropsTest } from '../../../../tests/providerTests/restPropsTest'; import { stylePropsTest } from '../../../../tests/providerTests/stylePropsTest'; +import { Button } from '../../Button'; import UncontrolledCollapse from '../UncontrolledCollapse'; describe('UncontrolledCollapse', () => { @@ -56,3 +57,30 @@ describe('UncontrolledCollapse', () => { expect(element).toHaveAttribute('id', 'example-id'); }); }); + +describe('UncontrolledCollapse with disposable trigger', () => { + const content = 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam, similique.'; + + it('should hide trigger after collapse open', () => { + render( +
+ {content} + } + isDisposable + > + {content} + +
, + ); + + const trigger = screen.getByRole('button') as HTMLElement; + + fireEvent.click(trigger); + + expect(trigger).not.toBeInTheDocument(); + }); +}); diff --git a/packages/web-react/src/components/Collapse/demo/CollapseUncontrolled.tsx b/packages/web-react/src/components/Collapse/demo/CollapseUncontrolled.tsx index 7403a9878a..a8bd06b411 100644 --- a/packages/web-react/src/components/Collapse/demo/CollapseUncontrolled.tsx +++ b/packages/web-react/src/components/Collapse/demo/CollapseUncontrolled.tsx @@ -1,19 +1,32 @@ -// Because there is no `dist` directory during the CI run -/* eslint-disable import/no-extraneous-dependencies, import/extensions, import/no-unresolved */ -import { StoryFn } from '@storybook/react'; import React from 'react'; +import { Button } from '../../Button'; import UncontrolledCollapse from '../UncontrolledCollapse'; -import { CollapseTrigger, content } from './Collapse'; -const Story: StoryFn = () => { +const UncontrolledCollapseDemo = () => { + const content = ( +

+ Condimentum odio, pulvinar et sollicitudin accumsan ac hendrerit vestibulum commodo, molestie laoreet dui sit + amet. Molestie consectetur, sed ac felis scelerisque lectus accumsan purus id dolor sed vitae, praesent aliquam + dolor quis ornare. Nulla sit amet, rhoncus at quis odio et iaculis lacinia suscipit vivamus sodales, nunc id + condimentum felis. Consectetur nec commodo, praesent et elit magna purus molestie cursus molestie, libero ut + venenatis erat id et nisi. Quam posuere, aliquam quam leo vitae tellus semper eget nunc, ultricies adipiscing sit + amet accumsan. Lorem rutrum, porttitor ante mauris suspendisse ultricies consequat purus, congue a commodo magna + et. +

+ ); + return (
{content} - + } + isDisposable + > {content}
); }; -export default Story; +export default UncontrolledCollapseDemo; diff --git a/packages/web-react/src/components/Collapse/stories/UncontrolledCollapse.stories.tsx b/packages/web-react/src/components/Collapse/stories/UncontrolledCollapse.stories.tsx index a2cd43610c..09fa329d3c 100644 --- a/packages/web-react/src/components/Collapse/stories/UncontrolledCollapse.stories.tsx +++ b/packages/web-react/src/components/Collapse/stories/UncontrolledCollapse.stories.tsx @@ -1,16 +1,21 @@ import type { Meta, StoryObj } from '@storybook/react'; import React from 'react'; +import { SpiritUncontrolledCollapseProps } from '../../../types'; import { Button } from '../../Button'; import { UncontrolledCollapse } from '..'; -import content from './content'; -const meta: Meta = { +type UncontrolledCollapsePlaygroundType = { + content: string; +} & SpiritUncontrolledCollapseProps; + +const meta: Meta = { title: 'Components/Collapse', component: UncontrolledCollapse, argTypes: { collapsibleToBreakpoint: { control: 'select', options: ['mobile', 'tablet', 'desktop', undefined], + description: 'Handle for responsive breakpoint', table: { defaultValue: { summary: undefined }, }, @@ -23,41 +28,74 @@ const meta: Meta = { }, hideOnCollapse: { control: 'boolean', + description: '**DEPRECATED** in favor of `isDisposable`; Hides button when content is displayed', + table: { + defaultValue: { summary: 'false' }, + }, + }, + isDisposable: { + control: 'boolean', + description: 'Hides trigger when content is displayed', + table: { + defaultValue: { summary: 'false' }, + }, }, id: { control: 'text', }, isOpen: { control: 'boolean', + description: 'Initial state of the collapse', + table: { + defaultValue: { summary: 'false' }, + }, }, transitionDuration: { control: 'number', + description: 'Duration of the transition', table: { - defaultValue: { summary: '250' }, + defaultValue: { summary: '250', detail: ' in milliseconds' }, }, }, + content: { + control: 'text', + description: 'Content to be displayed in the collapse', + }, }, args: { elementType: 'div', + hideOnCollapse: false, id: 'collapse-example', + isDisposable: false, isOpen: false, transitionDuration: 250, + content: + 'Condimentum odio, pulvinar et sollicitudin accumsan ac hendrerit vestibulum commodo, molestie laoreet dui sit amet. Molestie consectetur, sed ac felis scelerisque lectus accumsan purus id dolor sed vitae, praesent aliquam dolor quis ornare. Nulla sit amet, rhoncus at quis odio et iaculis lacinia suscipit vivamus sodales, nunc id condimentum felis. Consectetur nec commodo, praesent et elit magna purus molestie cursus molestie, libero ut venenatis erat id et nisi. Quam posuere, aliquam quam leo vitae tellus semper eget nunc, ultricies adipiscing sit amet accumsan. Lorem rutrum, porttitor ante mauris suspendisse ultricies consequat purus, congue a commodo magna et.', }, }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const UncontrolledCollapsePlayground: Story = { name: 'UncontrolledCollapse', - render: (args) => ( - ( - - )} - > - {content} - - ), + render: (args) => { + const { content } = args; + + return ( + <> +

{content}

+ ( + + )} + > +

{content}

+
+ + ); + }, }; diff --git a/packages/web-react/src/types/collapse.ts b/packages/web-react/src/types/collapse.ts index 9021fcd54d..6b7df026a0 100644 --- a/packages/web-react/src/types/collapse.ts +++ b/packages/web-react/src/types/collapse.ts @@ -28,7 +28,9 @@ export interface TransitionCollapseProps { export interface SpiritCollapseProps extends CollapseProps, TransitionCollapseProps {} export interface SpiritUncontrolledCollapseProps extends Omit { - isOpen?: boolean; + /** @deprecated "hideOnCollapse" property will be replaced in the next major version. Please use "isDisposable" instead. */ hideOnCollapse?: boolean; + isDisposable?: boolean; + isOpen?: boolean; renderTrigger?: (render: CollapseRenderProps) => ReactNode; } diff --git a/packages/web-twig/src/Resources/components/Collapse/README.md b/packages/web-twig/src/Resources/components/Collapse/README.md index 0ed09acdcc..049a0c6193 100644 --- a/packages/web-twig/src/Resources/components/Collapse/README.md +++ b/packages/web-twig/src/Resources/components/Collapse/README.md @@ -69,14 +69,14 @@ and [escape hatches][readme-escape-hatches]. ## Trigger attributes -| Name | Type | Default | Required | Description | -| --------------------------- | -------- | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------ | -| `aria-controls` | `string` | — | ✕ | Aria controls state (auto) | -| `aria-expanded` | `string` | — | ✕ | Aria expanded state (auto) | +| Name | Type | Default | Required | Description | +| --------------------------- | -------- | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------- | +| `aria-controls` | `string` | — | ✕ | Aria controls state (auto) | +| `aria-expanded` | `string` | — | ✕ | Aria expanded state (auto) | | `data-spirit-is-disposable` | `bool` | — | ✕ | For hide on collapse as more trigger | | `data-spirit-more` | `bool` | — | ✕ | [**DEPRECATED**][readme-deprecations] in favor of `data-spirit-is-disposable`; For hide on collapse as more trigger | -| `data-spirit-target` | `string` | — | ✓ | Target selector | -| `data-spirit-toggle` | `string` | `collapse` | ✓ | Iterable selector | +| `data-spirit-target` | `string` | — | ✓ | Target selector | +| `data-spirit-toggle` | `string` | `collapse` | ✓ | Iterable selector | Other necessary attributes are toggled automatically, like `aria-controls` and `aria-expanded` when component is loaded or width of window is changed. There can be several triggers, the same rules apply to each.