Skip to content

Commit e0972f3

Browse files
author
Eric Olkowski
committed
fix(ExpandableSection): made animation opt-in for detached variant
1 parent e62933d commit e0972f3

7 files changed

Lines changed: 130 additions & 89 deletions

File tree

packages/react-core/src/components/ExpandableSection/ExpandableSection.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export interface ExpandableSectionProps extends Omit<React.HTMLProps<HTMLDivElem
3232
toggleId?: string;
3333
/** Display size variant. Set to "lg" for disclosure styling. */
3434
displaySize?: 'default' | 'lg';
35-
/** Indicates the expandable section has a detached toggle. */
35+
/** Flag indicating that the expandable section and expandable toggle are detached from one another. */
3636
isDetached?: boolean;
3737
/** Flag to indicate if the content is expanded. */
3838
isExpanded?: boolean;
@@ -64,6 +64,10 @@ export interface ExpandableSectionProps extends Omit<React.HTMLProps<HTMLDivElem
6464
* variant, the expandable content will be truncated after 3 lines by default.
6565
*/
6666
variant?: 'default' | 'truncate';
67+
/** Sets the direction of the expandable animation when isDetached is true. If this prop is not passed,
68+
* animation will not occur.
69+
*/
70+
direction?: 'up' | 'down';
6771
}
6872

6973
interface ExpandableSectionState {
@@ -198,6 +202,7 @@ class ExpandableSection extends Component<ExpandableSectionProps, ExpandableSect
198202
variant,
199203
// eslint-disable-next-line @typescript-eslint/no-unused-vars
200204
truncateMaxLines,
205+
direction,
201206
...props
202207
} = this.props;
203208

@@ -258,6 +263,8 @@ class ExpandableSection extends Component<ExpandableSectionProps, ExpandableSect
258263
displaySize === 'lg' && styles.modifiers.displayLg,
259264
isWidthLimited && styles.modifiers.limitWidth,
260265
isIndented && styles.modifiers.indented,
266+
isDetached && 'pf-m-detached',
267+
isDetached && direction && (direction === 'up' ? styles.modifiers.expandTop : 'pf-m-expand-bottom'),
261268
variant === ExpandableSectionVariant.truncate && styles.modifiers.truncate,
262269
className
263270
)}

packages/react-core/src/components/ExpandableSection/ExpandableSectionToggle.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export interface ExpandableSectionToggleProps extends Omit<React.HTMLProps<HTMLD
2828
isExpanded?: boolean;
2929
/** Callback function to toggle the expandable content. */
3030
onToggle?: (isExpanded: boolean) => void;
31+
/** Flag indicating that the expandable section and expandable toggle are detached from one another. */
32+
isDetached?: boolean;
3133
}
3234

3335
export const ExpandableSectionToggle: React.FunctionComponent<ExpandableSectionToggleProps> = ({
@@ -39,12 +41,14 @@ export const ExpandableSectionToggle: React.FunctionComponent<ExpandableSectionT
3941
toggleId,
4042
direction = 'down',
4143
hasTruncatedContent = false,
44+
isDetached = true,
4245
...props
4346
}: ExpandableSectionToggleProps) => (
4447
<div
4548
className={css(
4649
styles.expandableSection,
4750
isExpanded && styles.modifiers.expanded,
51+
isDetached && 'pf-m-detached',
4852
hasTruncatedContent && styles.modifiers.truncate,
4953
className
5054
)}
@@ -63,7 +67,7 @@ export const ExpandableSectionToggle: React.FunctionComponent<ExpandableSectionT
6367
<span
6468
className={css(
6569
styles.expandableSectionToggleIcon,
66-
isExpanded && direction === 'up' && styles.modifiers.expandTop
70+
isExpanded && direction === 'up' && styles.modifiers.expandTop // TODO: next breaking change move this class to the outter styles.expandableSection wrapper
6771
)}
6872
>
6973
<AngleRightIcon />

packages/react-core/src/components/ExpandableSection/__tests__/ExpandableSection.test.tsx

Lines changed: 81 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { Fragment } from 'react';
21
import { render, screen } from '@testing-library/react';
32
import userEvent from '@testing-library/user-event';
43

54
import { ExpandableSection, ExpandableSectionVariant } from '../ExpandableSection';
6-
import { ExpandableSectionToggle } from '../ExpandableSectionToggle';
5+
import styles from '@patternfly/react-styles/css/components/ExpandableSection/expandable-section';
76

87
const props = { contentId: 'content-id', toggleId: 'toggle-id' };
98

@@ -22,7 +21,7 @@ test('Renders ExpandableSection expanded', () => {
2221
expect(asFragment()).toMatchSnapshot();
2322
});
2423

25-
test('ExpandableSection onToggle called', async () => {
24+
test('Calls onToggle when clicked', async () => {
2625
const mockfn = jest.fn();
2726
const user = userEvent.setup();
2827

@@ -32,6 +31,21 @@ test('ExpandableSection onToggle called', async () => {
3231
expect(mockfn.mock.calls).toHaveLength(1);
3332
});
3433

34+
test('Does not call onToggle when not clicked', async () => {
35+
const mockfn = jest.fn();
36+
const user = userEvent.setup();
37+
38+
render(
39+
<>
40+
<ExpandableSection onToggle={mockfn}> test </ExpandableSection>
41+
<button onClick={() => {}}>Test clicker</button>
42+
</>
43+
);
44+
45+
await user.click(screen.getByRole('button', { name: 'Test clicker' }));
46+
expect(mockfn).not.toHaveBeenCalled();
47+
});
48+
3549
test('Renders Uncontrolled ExpandableSection', () => {
3650
const { asFragment } = render(
3751
<ExpandableSection {...props} toggleText="Show More">
@@ -42,20 +56,6 @@ test('Renders Uncontrolled ExpandableSection', () => {
4256
expect(asFragment()).toMatchSnapshot();
4357
});
4458

45-
test('Detached ExpandableSection renders successfully', () => {
46-
const { asFragment } = render(
47-
<Fragment>
48-
<ExpandableSection isExpanded isDetached {...props}>
49-
test
50-
</ExpandableSection>
51-
<ExpandableSectionToggle isExpanded direction="up" {...props}>
52-
Toggle text
53-
</ExpandableSectionToggle>
54-
</Fragment>
55-
);
56-
expect(asFragment()).toMatchSnapshot();
57-
});
58-
5959
test('Disclosure ExpandableSection', () => {
6060
const { asFragment } = render(
6161
<ExpandableSection {...props} displaySize="lg" isWidthLimited>
@@ -75,22 +75,22 @@ test('Renders ExpandableSection indented', () => {
7575
expect(asFragment()).toMatchSnapshot();
7676
});
7777

78-
test('Does not render with pf-m-truncate class when variant is not passed', () => {
78+
test(`Does not render with ${styles.modifiers.truncate} class when variant is not passed`, () => {
7979
render(<ExpandableSection>test</ExpandableSection>);
8080

81-
expect(screen.getByText('test').parentElement).not.toHaveClass('pf-m-truncate');
81+
expect(screen.getByText('test').parentElement).not.toHaveClass(styles.modifiers.truncate);
8282
});
8383

84-
test('Does not render with pf-m-truncate class when variant is not truncate', () => {
84+
test(`Does not render with ${styles.modifiers.truncate} class when variant is not truncate`, () => {
8585
render(<ExpandableSection variant={ExpandableSectionVariant.default}>test</ExpandableSection>);
8686

87-
expect(screen.getByText('test').parentElement).not.toHaveClass('pf-m-truncate');
87+
expect(screen.getByText('test').parentElement).not.toHaveClass(styles.modifiers.truncate);
8888
});
8989

90-
test('Renders with pf-m-truncate class when variant is truncate', () => {
90+
test(`Renders with ${styles.modifiers.truncate} class when variant is truncate`, () => {
9191
render(<ExpandableSection variant={ExpandableSectionVariant.truncate}>test</ExpandableSection>);
9292

93-
expect(screen.getByText('test').parentElement).toHaveClass('pf-m-truncate');
93+
expect(screen.getByText('test').parentElement).toHaveClass(styles.modifiers.truncate);
9494
});
9595

9696
test('Renders with value passed to contentId', () => {
@@ -129,3 +129,61 @@ test('Renders with ARIA attributes when contentId and toggleId are passed', () =
129129
expect(wrapper).toContainHTML('aria-labelledby="toggle-id"');
130130
expect(wrapper).toContainHTML('aria-controls="content-id"');
131131
});
132+
133+
test(`Does not render with class pf-m-detached by default`, () => {
134+
render(<ExpandableSection>Test content</ExpandableSection>);
135+
136+
expect(screen.getByText('Test content').parentElement).not.toHaveClass('pf-m-detached');
137+
});
138+
139+
test(`Renders with class pf-m-detached when isDetached is true`, () => {
140+
render(<ExpandableSection isDetached>Test content</ExpandableSection>);
141+
142+
expect(screen.getByText('Test content').parentElement).toHaveClass('pf-m-detached');
143+
});
144+
145+
test(`Does not render with classes pf-m-expand-top nor pf-m-expand-bottom by default`, () => {
146+
render(<ExpandableSection>Test content</ExpandableSection>);
147+
148+
expect(screen.getByText('Test content').parentElement).not.toHaveClass('pf-m-expand-top');
149+
expect(screen.getByText('Test content').parentElement).not.toHaveClass('pf-m-expand-bottom');
150+
});
151+
152+
test(`Does not render with classes pf-m-expand-top nor pf-m-expand-bottom when only isDetached is true`, () => {
153+
render(<ExpandableSection isDetached>Test content</ExpandableSection>);
154+
155+
expect(screen.getByText('Test content').parentElement).not.toHaveClass('pf-m-expand-top');
156+
expect(screen.getByText('Test content').parentElement).not.toHaveClass('pf-m-expand-bottom');
157+
});
158+
159+
test(`Does not render with class pf-m-expand-top when only direction="up"`, () => {
160+
render(<ExpandableSection direction="up">Test content</ExpandableSection>);
161+
162+
expect(screen.getByText('Test content').parentElement).not.toHaveClass('pf-m-expand-top');
163+
});
164+
165+
test(`Does not render with class pf-m-expand-bottom when only direction="down"`, () => {
166+
render(<ExpandableSection direction="down">Test content</ExpandableSection>);
167+
168+
expect(screen.getByText('Test content').parentElement).not.toHaveClass('pf-m-expand-bottom');
169+
});
170+
171+
test(`Renders with class pf-m-expand-top when isDetached is true and direction="up"`, () => {
172+
render(
173+
<ExpandableSection isDetached direction="up">
174+
Test content
175+
</ExpandableSection>
176+
);
177+
178+
expect(screen.getByText('Test content').parentElement).toHaveClass('pf-m-expand-top');
179+
});
180+
181+
test(`Renders with class pf-m-expand-bottom when isDetached is true and direction="down"`, () => {
182+
render(
183+
<ExpandableSection isDetached direction="down">
184+
Test content
185+
</ExpandableSection>
186+
);
187+
188+
expect(screen.getByText('Test content').parentElement).toHaveClass('pf-m-expand-bottom');
189+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { render, screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import { ExpandableSectionToggle } from '../ExpandableSectionToggle';
4+
import styles from '@patternfly/react-styles/css/components/ExpandableSection/expandable-section';
5+
6+
test('Renders without children', () => {
7+
render(<ExpandableSectionToggle></ExpandableSectionToggle>);
8+
9+
expect(screen.getByRole('button')).toBeInTheDocument();
10+
});
11+
12+
test('Renders with children', () => {
13+
render(<ExpandableSectionToggle>Toggle test</ExpandableSectionToggle>);
14+
15+
expect(screen.getByRole('button')).toHaveTextContent('Toggle test');
16+
});
17+
18+
test('Renders with class pf-m-detached by default', () => {
19+
render(<ExpandableSectionToggle data-testid="section-wrapper">Toggle test</ExpandableSectionToggle>);
20+
21+
expect(screen.getByTestId('section-wrapper')).toHaveClass('pf-m-detached');
22+
});
23+
24+
test('Does not render with class pf-m-detached when isDetached is false', () => {
25+
render(
26+
<ExpandableSectionToggle isDetached={false} data-testid="section-wrapper">
27+
Toggle test
28+
</ExpandableSectionToggle>
29+
);
30+
31+
expect(screen.getByTestId('section-wrapper')).not.toHaveClass('pf-m-detached');
32+
});

packages/react-core/src/components/ExpandableSection/__tests__/__snapshots__/ExpandableSection.test.tsx.snap

Lines changed: 1 addition & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,5 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`Detached ExpandableSection renders successfully 1`] = `
4-
<DocumentFragment>
5-
<div
6-
class="pf-v6-c-expandable-section pf-m-expanded"
7-
>
8-
<div
9-
aria-labelledby="toggle-id"
10-
class="pf-v6-c-expandable-section__content"
11-
id="content-id"
12-
role="region"
13-
>
14-
test
15-
</div>
16-
</div>
17-
<div
18-
class="pf-v6-c-expandable-section pf-m-expanded"
19-
>
20-
<div
21-
class="pf-v6-c-expandable-section__toggle"
22-
>
23-
<button
24-
aria-controls="content-id"
25-
aria-expanded="true"
26-
class="pf-v6-c-button pf-m-link"
27-
data-ouia-component-id="OUIA-Generated-Button-link-5"
28-
data-ouia-component-type="PF6/Button"
29-
data-ouia-safe="true"
30-
id="toggle-id"
31-
type="button"
32-
>
33-
<span
34-
class="pf-v6-c-button__icon pf-m-start"
35-
>
36-
<span
37-
class="pf-v6-c-expandable-section__toggle-icon pf-m-expand-top"
38-
>
39-
<svg
40-
aria-hidden="true"
41-
class="pf-v6-svg"
42-
fill="currentColor"
43-
height="1em"
44-
role="img"
45-
viewBox="0 0 256 512"
46-
width="1em"
47-
>
48-
<path
49-
d="M224.3 273l-136 136c-9.4 9.4-24.6 9.4-33.9 0l-22.6-22.6c-9.4-9.4-9.4-24.6 0-33.9l96.4-96.4-96.4-96.4c-9.4-9.4-9.4-24.6 0-33.9L54.3 103c9.4-9.4 24.6-9.4 33.9 0l136 136c9.5 9.4 9.5 24.6.1 34z"
50-
/>
51-
</svg>
52-
</span>
53-
</span>
54-
<span
55-
class="pf-v6-c-button__text"
56-
>
57-
Toggle text
58-
</span>
59-
</button>
60-
</div>
61-
</div>
62-
</DocumentFragment>
63-
`;
64-
653
exports[`Disclosure ExpandableSection 1`] = `
664
<DocumentFragment>
675
<div
@@ -285,7 +223,7 @@ exports[`Renders Uncontrolled ExpandableSection 1`] = `
285223
<button
286224
aria-controls="content-id"
287225
class="pf-v6-c-button pf-m-link"
288-
data-ouia-component-id="OUIA-Generated-Button-link-4"
226+
data-ouia-component-id="OUIA-Generated-Button-link-5"
289227
data-ouia-component-type="PF6/Button"
290228
data-ouia-safe="true"
291229
id="toggle-id"

packages/react-core/src/components/ExpandableSection/examples/ExpandableSection.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import CheckCircleIcon from '@patternfly/react-icons/dist/esm/icons/check-circle
3232

3333
When passing the `isDetached` property into `<ExpandableSection>`, you must also manually pass in the same `toggleId` and `contentId` properties to both `<ExpandableSection>` and `<ExpandableSectionToggle>`. This will link the content to the toggle via ARIA attributes.
3434

35+
By default animations will not be enabled for a detached `<ExpandableSection>`. You must manually pass the `direction` property with an appropriate value based on where the expandable content is rendered. If the expandable content is above the expandable toggle, `direction="up"` must be passed like in this example.
36+
3537
```ts file="ExpandableSectionDetached.tsx"
3638

3739
```

packages/react-core/src/components/ExpandableSection/examples/ExpandableSectionDetached.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export const ExpandableSectionDetached: React.FunctionComponent = () => {
1313
return (
1414
<Stack hasGutter>
1515
<StackItem>
16-
<ExpandableSection isExpanded={isExpanded} isDetached toggleId={toggleId} contentId={contentId}>
16+
<ExpandableSection isExpanded={isExpanded} isDetached direction="up" toggleId={toggleId} contentId={contentId}>
1717
This content is visible only when the component is expanded.
1818
</ExpandableSection>
1919
</StackItem>

0 commit comments

Comments
 (0)