Skip to content
This repository was archived by the owner on Sep 30, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/good-eels-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/polaris': minor
---

Added transition delay to Collapsible
45 changes: 37 additions & 8 deletions polaris-react/src/components/Collapsible/Collapsible.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const Default = {
);
},
};

export const Inline = {
render() {
const [open, setOpen] = useState(true);
Expand All @@ -70,14 +71,44 @@ export const Inline = {
<Button
onClick={handleToggle}
ariaExpanded={open}
ariaControls="basic-collapsible"
ariaControls="inline-collapsible"
>
Toggle
</Button>
<Collapsible open={open} id="inline-collapsible" variant="inline">
<p style={{whiteSpace: 'nowrap', backgroundColor: 'red'}}>
Non breaking text
</p>
<p style={{whiteSpace: 'nowrap'}}>Non breaking text</p>
</Collapsible>
</LegacyStack>
</LegacyCard>
</div>
);
},
};

export const WithDelay = {
render() {
const [open, setOpen] = useState(true);

const handleToggle = useCallback(() => setOpen((open) => !open), []);

return (
<div style={{height: '200px'}}>
<LegacyCard sectioned>
<LegacyStack alignment="center">
<Button
onClick={handleToggle}
ariaExpanded={open}
ariaControls="inline-collapsible"
>
Toggle
</Button>
<Collapsible
open={open}
id="inline-collapsible"
variant="inline"
transition={{delay: '500'}}
>
<p style={{whiteSpace: 'nowrap'}}>Non breaking text</p>
</Collapsible>
</LegacyStack>
</LegacyCard>
Expand Down Expand Up @@ -119,9 +150,7 @@ export const AnimateIn = {
duration: 'var(--p-motion-duration-250)',
}}
>
<p style={{whiteSpace: 'nowrap', backgroundColor: 'red'}}>
Non breaking text
</p>
<p style={{whiteSpace: 'nowrap'}}>Non breaking text</p>
</Collapsible>

<Button
Expand All @@ -141,7 +170,7 @@ export const AnimateIn = {
<Box maxWidth="20%">
<Collapsible
open={open}
id="inline-collapsible"
id="basic-collapsible"
transition={{
animateIn: true,
duration: 'var(--p-motion-duration-250)',
Expand Down
11 changes: 8 additions & 3 deletions polaris-react/src/components/Collapsible/Collapsible.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import React, {useState, useRef, useEffect, useCallback} from 'react';
import type {ReactNode, TransitionEvent} from 'react';
import {createVar} from '@shopify/polaris-tokens';
import type {MotionDurationScale} from '@shopify/polaris-tokens';

import {classNames} from '../../utilities/css';

Expand All @@ -7,6 +10,8 @@ import styles from './Collapsible.module.css';
interface Transition {
/** Expand the collpsible on render. */
animateIn?: boolean;
/** Assign a transition delay to the collapsible animation */
delay?: MotionDurationScale;
/** Assign a transition duration to the collapsible animation. */
duration?: string;
/** Assign a transition timing function to the collapsible animation */
Expand All @@ -31,7 +36,7 @@ export interface CollapsibleProps {
/** Callback when the animation completes. */
onAnimationEnd?(): void;
/** The content to display inside the collapsible. */
children?: React.ReactNode;
children?: ReactNode;
}

type AnimationState = 'idle' | 'measuring' | 'animating';
Expand All @@ -52,7 +57,6 @@ export function Collapsible({
const [animationState, setAnimationState] = useState<AnimationState>(
animateIn ? 'measuring' : 'idle',
);

const isFullyOpen = animationState === 'idle' && open && isOpen;
const isFullyClosed = animationState === 'idle' && !open && !isOpen;
const content = expandOnPrint || !isFullyClosed ? children : null;
Expand All @@ -69,6 +73,7 @@ export function Collapsible({
const transitionDisabled = isTransitionDisabled(transition);

const transitionStyles = typeof transition === 'object' && {
transitionDelay: createVar(`motion-duration-${transition.delay ?? '0'}`),
transitionDuration: transition.duration,
transitionTimingFunction: transition.timingFunction,
};
Expand All @@ -87,7 +92,7 @@ export function Collapsible({
};

const handleCompleteAnimation = useCallback(
({target}: React.TransitionEvent<HTMLDivElement>) => {
({target}: TransitionEvent<HTMLDivElement>) => {
if (target === collapsibleContainer.current) {
setAnimationState('idle');
setIsOpen(open);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,11 @@ describe('<Collapsible />', () => {
<Collapsible id="test-collapsible" open transition={{duration}} />,
);

expect(collapsible).toHaveReactProps({transition: {duration}});
expect(collapsible).toContainReactComponent('div', {
style: expect.objectContaining({
transitionDuration: duration,
}),
});
});

it('passes a timingFunction property', () => {
Expand All @@ -165,7 +169,24 @@ describe('<Collapsible />', () => {
/>,
);

expect(collapsible).toHaveReactProps({transition: {timingFunction}});
expect(collapsible).toContainReactComponent('div', {
style: expect.objectContaining({
transitionTimingFunction: timingFunction,
}),
});
});

it('passes a delay property', () => {
const delay = '100';
const collapsible = mountWithApp(
<Collapsible id="test-collapsible" open transition={{delay}} />,
);

expect(collapsible).toContainReactComponent('div', {
style: expect.objectContaining({
transitionDelay: `var(--p-motion-duration-${delay})`,
}),
});
});

const transitionDisabledOptions = [
Expand Down