Skip to content

Commit

Permalink
Refactor(web-react): Collapse API prop changed
Browse files Browse the repository at this point in the history
- prop `hideOnCollapse`` changed to `isDisposable`

- Solves DS-832
  • Loading branch information
pavelklibani committed Dec 9, 2024
1 parent f09a740 commit 9935692
Show file tree
Hide file tree
Showing 9 changed files with 266 additions and 41 deletions.
13 changes: 11 additions & 2 deletions packages/web-react/DEPRECATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

- `<UncontrolledCollapse id="collapse" renderTrigger={…} hideOnCollapse … />``<UncontrolledCollapse id="collapse" renderTrigger={…} isDisposable … />`

[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
27 changes: 19 additions & 8 deletions packages/web-react/src/components/Collapse/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -31,7 +40,7 @@ const UncontrolledCollapse = (props: SpiritUncontrolledCollapseProps) => {
return (
<>
{triggerRenderHandler()}
{hideOnCollapse && isOpen ? (
{isDisposed && isOpen ? (
children
) : (
<Collapse {...restProps} isOpen={isOpen}>
Expand Down
115 changes: 115 additions & 0 deletions packages/web-react/src/components/Collapse/__tests__/Collapse.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<Collapse id="collapse" isOpen data-testId="test">
Hello World
</Collapse>,
);

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 (
<>
<Button type="button" data-testId="test-button" onClick={() => setIsOpen(!isOpen)}>
Toggle Collapse
</Button>
<Collapse id="collapse" isOpen={isOpen} data-testId="test">
Hello World
</Collapse>
</>
);
};

render(<RenderCollapse />);

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(
<Collapse id="collapse" elementType="section" isOpen data-testId="test">
Hello World
</Collapse>,
);

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 (
<>
<Button type="button" data-testId="test-button" onClick={() => setIsOpen(!isOpen)}>
Toggle Collapse
</Button>
<Collapse id="collapse" isOpen={isOpen} transitionDuration={duration} data-testId="test">
Hello World
</Collapse>
</>
);
};

const transitionDuration = 250;

render(<RenderCollapse duration={transitionDuration} />);

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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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(
<div>
{content}
<UncontrolledCollapse
id="uncontrolled-collapse-id"
// Normally we want to display state change, not in this test, prop is passed anyway
// eslint-disable-next-line @typescript-eslint/no-unused-vars
renderTrigger={({ isOpen, ...rest }) => <Button {...rest}>… more</Button>}
isDisposable
>
{content}
</UncontrolledCollapse>
</div>,
);

const trigger = screen.getByRole('button') as HTMLElement;

fireEvent.click(trigger);

expect(trigger).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -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<typeof UncontrolledCollapse> = () => {
const UncontrolledCollapseDemo = () => {
const content = (
<p>
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.
</p>
);

return (
<div>
{content}
<UncontrolledCollapse id="uncontrolled-collapse-id" renderTrigger={CollapseTrigger}>
<UncontrolledCollapse
id="uncontrolled-collapse-id"
renderTrigger={(props) => <Button {...props}>… more</Button>}
isDisposable
>
{content}
</UncontrolledCollapse>
</div>
);
};

export default Story;
export default UncontrolledCollapseDemo;
Loading

0 comments on commit 9935692

Please sign in to comment.