Skip to content

Commit 91344fd

Browse files
committed
feat(Toaster): add experimental animation timing function
1 parent eec8953 commit 91344fd

File tree

7 files changed

+103
-31
lines changed

7 files changed

+103
-31
lines changed

src/components/Toaster/ToastList/ToastAnimation.scss

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,43 @@ $animation-duration: 0.6s;
6161
animation-name: #{variables.$ns}toast-exit-reduced-motion;
6262
}
6363
}
64+
65+
// Alternative animation timing function
66+
&_feature_alternate-animation-timing-fn {
67+
animation-timing-function: linear(
68+
0,
69+
0.002 0.3%,
70+
0.007 0.6%,
71+
0.033 1.3%,
72+
0.073 2%,
73+
0.125 2.7%,
74+
0.254 4.1%,
75+
0.683 8.3%,
76+
0.803 9.7%,
77+
0.897 11%,
78+
0.977 12.4%,
79+
1.036 13.8%,
80+
1.058 14.5%,
81+
1.078 15.3%,
82+
1.091 16%,
83+
1.101 16.8%,
84+
1.106 17.5%,
85+
1.108 18.3%,
86+
1.107 19.2%,
87+
1.103 20.1%,
88+
1.089 21.8%,
89+
1.038 26.5%,
90+
1.014 29.1%,
91+
1.005 30.5%,
92+
0.997 32%,
93+
0.992 33.5%,
94+
0.989 35.1%,
95+
0.989 38.6%,
96+
0.998 47.4%,
97+
1.001 53.2%,
98+
1
99+
);
100+
}
64101
}
65102

66103
@keyframes #{variables.$ns}toast-enter-#{$platform} {

src/components/Toaster/ToastList/ToastList.tsx

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,39 @@
11
'use client';
22

3+
import * as React from 'react';
4+
35
import {CSSTransition, TransitionGroup} from 'react-transition-group';
46

57
import {block} from '../../utils/cn';
68
import {getCSSTransitionClassNames} from '../../utils/transition';
79
import {Toast} from '../Toast/Toast';
8-
import type {InternalToastProps} from '../types';
10+
import type {InternalToastProps, ToastListProps} from '../types';
911

1012
import './ToastAnimation.scss';
1113
import './ToastList.scss';
1214

13-
const desktopTransitionClassNames = getCSSTransitionClassNames(block('toast-animation-desktop'));
14-
const mobileTransitionClassNames = getCSSTransitionClassNames(block('toast-animation-mobile'));
15-
16-
type ToastListProps = {
17-
removeCallback: (name: string) => void;
18-
toasts: InternalToastProps[];
19-
mobile?: boolean;
20-
};
21-
2215
export function ToastList(props: ToastListProps) {
23-
const {toasts, mobile, removeCallback} = props;
16+
const {toasts, mobile, alternateAnimationFunction, removeCallback} = props;
17+
18+
const classNames = React.useMemo(
19+
() =>
20+
mobile
21+
? getCSSTransitionClassNames(block('toast-animation-mobile'), {
22+
feature: alternateAnimationFunction ? 'alternate-animation-timing-fn' : false,
23+
})
24+
: getCSSTransitionClassNames(block('toast-animation-desktop'), {
25+
feature: alternateAnimationFunction ? 'alternate-animation-timing-fn' : false,
26+
}),
27+
[alternateAnimationFunction, mobile],
28+
);
2429

2530
return (
2631
<TransitionGroup component={null}>
2732
{toasts.map((toast) => (
2833
<CSSTransition
2934
key={`${toast.name}_${toast.addedAt}`}
3035
nodeRef={toast.ref}
31-
classNames={mobile ? mobileTransitionClassNames : desktopTransitionClassNames}
36+
classNames={classNames}
3237
addEndListener={(done) =>
3338
toast.ref?.current?.addEventListener('animationend', done)
3439
}

src/components/Toaster/ToasterComponent/ToasterComponent.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,32 @@ import {block} from '../../utils/cn';
88
import {ToastsContext} from '../Provider/ToastsContext';
99
import {ToastList} from '../ToastList/ToastList';
1010
import {useToaster} from '../hooks/useToaster';
11+
import type {ToastListProps} from '../types';
1112

12-
interface Props {
13+
interface Props extends Pick<ToastListProps, 'mobile' | 'alternateAnimationFunction'> {
1314
className?: string;
14-
mobile?: boolean;
1515
hasPortal?: boolean;
1616
}
1717

1818
const b = block('toaster');
1919

20-
export function ToasterComponent({className, mobile, hasPortal = true}: Props) {
20+
export function ToasterComponent({
21+
className,
22+
mobile,
23+
alternateAnimationFunction,
24+
hasPortal = true,
25+
}: Props) {
2126
const defaultMobile = useMobile();
2227
const {remove} = useToaster();
2328
const list = React.useContext(ToastsContext);
2429

2530
const toaster = (
26-
<ToastList toasts={list} removeCallback={remove} mobile={mobile ?? defaultMobile} />
31+
<ToastList
32+
toasts={list}
33+
removeCallback={remove}
34+
mobile={mobile ?? defaultMobile}
35+
alternateAnimationFunction={alternateAnimationFunction}
36+
/>
2737
);
2838

2939
if (!hasPortal) {

src/components/Toaster/__stories__/Toaster.stories.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,18 @@ const views: ButtonView[] = [
3131
'flat-contrast',
3232
];
3333

34-
function viewSelect(name: string) {
34+
function selectControl<T>(name: string, options: T[]) {
3535
return {
3636
name,
3737
control: 'select' as const,
38-
defaultValue: 'outlined',
39-
options: views,
40-
if: {arg: 'setActions'},
38+
options,
4139
};
4240
}
4341

42+
function viewSelect(name: string) {
43+
return {...selectControl(name, views), defaultValue: 'outlined', if: {arg: 'setActions'}};
44+
}
45+
4446
const disabledControl = {
4547
table: {
4648
disable: true,
@@ -95,6 +97,7 @@ export const Default: Story = {
9597
allowAutoHiding: booleanControl('Allow auto hiding'),
9698
setTitle: booleanControl('Add title'),
9799
setContent: booleanControl('Add content'),
100+
animation: selectControl('Animation', ['default', 'alternate']),
98101
setActions: booleanControl('Add action'),
99102
action1View: viewSelect('Action 1 view'),
100103
action2View: viewSelect('Action 2 view'),
@@ -123,6 +126,7 @@ export const ToastPlayground: Story = {
123126
actions: faker.helpers.uniqueArray(getAction, faker.number.int({min: 1, max: 2})),
124127
},
125128
argTypes: {
129+
animation: selectControl('Animation', ['default', 'alternate']),
126130
name: disabledControl,
127131
addedAt: disabledControl,
128132
renderIcon: disabledControl,
@@ -145,6 +149,7 @@ export const ToastPlayground: Story = {
145149
<ToasterComponent
146150
mobile={args.mobile}
147151
hasPortal={context.globals.screenshotTests !== true}
152+
alternateAnimationFunction={args.animation === 'alternate'}
148153
/>
149154
);
150155
},

src/components/Toaster/__stories__/ToasterShowcase.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ interface Props {
3737
showCloseIcon: boolean;
3838
setTimeout: boolean;
3939
allowAutoHiding: boolean;
40+
animation: 'default' | 'alternate';
4041
setTitle: boolean;
4142
setContent: boolean;
4243
setActions: boolean;
@@ -51,6 +52,7 @@ export const ToasterDemo = ({
5152
showCloseIcon,
5253
setTimeout,
5354
allowAutoHiding,
55+
animation,
5456
setTitle,
5557
setContent,
5658
setActions,
@@ -388,7 +390,10 @@ export const ToasterDemo = ({
388390
</Button>
389391
);
390392

391-
const component = React.useMemo(() => <ToasterComponent />, []);
393+
const component = React.useMemo(
394+
() => <ToasterComponent alternateAnimationFunction={animation === 'alternate'} />,
395+
[animation],
396+
);
392397

393398
return (
394399
<React.Fragment>

src/components/Toaster/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,11 @@ export interface ToasterContextMethods {
4646
}
4747

4848
export interface ToasterPublicMethods extends ToasterContextMethods {}
49+
50+
export type ToastListProps = {
51+
removeCallback: (name: string) => void;
52+
toasts: InternalToastProps[];
53+
mobile?: boolean;
54+
/** Experimental animation timing function */
55+
alternateAnimationFunction?: boolean;
56+
};

src/components/utils/transition.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1+
import type {NoStrictEntityMods} from '@bem-react/classname';
2+
13
import {modsClassName} from './cn';
24
import type {CnBlock} from './cn';
35

4-
export function getCSSTransitionClassNames(b: CnBlock) {
6+
export function getCSSTransitionClassNames(b: CnBlock, mods?: NoStrictEntityMods) {
57
return {
6-
appear: modsClassName(b({appear: true})),
7-
appearActive: modsClassName(b({appear: 'active'})),
8-
appearDone: modsClassName(b({appear: 'done'})),
9-
enter: modsClassName(b({enter: true})),
10-
enterActive: modsClassName(b({enter: 'active'})),
11-
enterDone: modsClassName(b({enter: 'done'})),
12-
exit: modsClassName(b({exit: true})),
13-
exitActive: modsClassName(b({exit: 'active'})),
14-
exitDone: modsClassName(b({exit: 'done'})),
8+
appear: modsClassName(b({...mods, appear: true})),
9+
appearActive: modsClassName(b({...mods, appear: 'active'})),
10+
appearDone: modsClassName(b({...mods, appear: 'done'})),
11+
enter: modsClassName(b({...mods, enter: true})),
12+
enterActive: modsClassName(b({...mods, enter: 'active'})),
13+
enterDone: modsClassName(b({...mods, enter: 'done'})),
14+
exit: modsClassName(b({...mods, exit: true})),
15+
exitActive: modsClassName(b({...mods, exit: 'active'})),
16+
exitDone: modsClassName(b({...mods, exit: 'done'})),
1517
};
1618
}

0 commit comments

Comments
 (0)