From 021932e357edabfeab18a98976f42a275ce64fae Mon Sep 17 00:00:00 2001 From: Tal Koren Date: Thu, 21 Mar 2024 15:12:31 +0200 Subject: [PATCH 1/6] feat(Label): scale animation in clickable variant --- packages/core/src/components/Label/Label.module.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/core/src/components/Label/Label.module.scss b/packages/core/src/components/Label/Label.module.scss index 3fd88972a6..536efb0fe4 100644 --- a/packages/core/src/components/Label/Label.module.scss +++ b/packages/core/src/components/Label/Label.module.scss @@ -4,6 +4,11 @@ .clickable { @include clickable.clickable; + + &:active { + transform: scale(0.95) translate3d(0, 0, 0); + transition: var(--motion-productive-short) transform; + } } .label { From 90286e98a29c2bd03732fdf41ad1aef9df0dbfbc Mon Sep 17 00:00:00 2001 From: Tal Koren Date: Thu, 21 Mar 2024 18:30:26 +0200 Subject: [PATCH 2/6] feat(Label): celebration animation --- packages/core/src/components/Label/Label.tsx | 41 ++++--- .../LabelCelebrationAnimation.module.scss | 91 ++++++++++++++ .../Label/LabelCelebrationAnimation.tsx | 112 ++++++++++++++++++ 3 files changed, 229 insertions(+), 15 deletions(-) create mode 100644 packages/core/src/components/Label/LabelCelebrationAnimation.module.scss create mode 100644 packages/core/src/components/Label/LabelCelebrationAnimation.tsx diff --git a/packages/core/src/components/Label/Label.tsx b/packages/core/src/components/Label/Label.tsx index 9d28acdff6..e5bced2c3e 100644 --- a/packages/core/src/components/Label/Label.tsx +++ b/packages/core/src/components/Label/Label.tsx @@ -11,6 +11,7 @@ import { VibeComponent, VibeComponentProps, withStaticProps } from "../../types" import useClickableProps from "../../hooks/useClickableProps/useClickableProps"; import useMergeRef from "../../hooks/useMergeRef"; import styles from "./Label.module.scss"; +import LabelCelebrationAnimation from "./LabelCelebrationAnimation"; export interface LabelProps extends VibeComponentProps { /** @@ -27,6 +28,7 @@ export interface LabelProps extends VibeComponentProps { isAnimationDisabled?: boolean; isLegIncluded?: boolean; onClick?: (event: React.MouseEvent) => void; + celebration?: boolean; } const Label: VibeComponent & { @@ -45,7 +47,8 @@ const Label: VibeComponent & { isLegIncluded = false, id, "data-testid": dataTestId, - onClick + onClick, + celebration }, ref ) => { @@ -62,12 +65,12 @@ const Label: VibeComponent & { getStyle(styles, camelCase("kind" + "-" + kind)), getStyle(styles, camelCase("color" + "-" + color)), { - [styles.withAnimation]: !isAnimationDisabled, + [styles.withAnimation]: !isAnimationDisabled && !celebration, [styles.withLeg]: isLegIncluded }, labelClassName ), - [kind, color, isAnimationDisabled, isLegIncluded, labelClassName] + [kind, color, isAnimationDisabled, isLegIncluded, labelClassName, celebration] ); const onClickCallback = useCallback( @@ -92,19 +95,27 @@ const Label: VibeComponent & { ); return ( - - - - {text} + + + + + {text} + + {isLegIncluded ? : null} - {isLegIncluded ? : null} - - + + ); } ); diff --git a/packages/core/src/components/Label/LabelCelebrationAnimation.module.scss b/packages/core/src/components/Label/LabelCelebrationAnimation.module.scss new file mode 100644 index 0000000000..b769d79c05 --- /dev/null +++ b/packages/core/src/components/Label/LabelCelebrationAnimation.module.scss @@ -0,0 +1,91 @@ +.celebration { + // Fallback perimeter, changes according to the path length + --container-perimeter: 840; + position: relative; + + .svg { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + + .stroke { + fill: none; + stroke-width: 1; + stroke-linecap: round; + stroke-linejoin: round; + animation: stroke-rotate cubic-bezier(0.33, 0, 0.67, 1) forwards; + stroke-dasharray: var(--container-perimeter); + stroke-dashoffset: var(--container-perimeter); + + &.base { + stroke: #ffcc00; + animation: fade 200ms linear forwards; + animation-delay: 80ms; + stroke-dasharray: 0; + stroke-dashoffset: 0; + opacity: 0; + } + + &.first { + stroke: #00ca72; + animation-delay: 80ms; + animation-duration: 320ms; + } + + &.second { + stroke: #fb275d; + animation-delay: 200ms; + animation-duration: 320ms; + } + + &.third { + stroke: var(--primary-color); + animation-delay: 360ms; + animation-duration: 320ms; + } + } + } + + [data-celebration-text] { + background-size: 300% 100%; + background-repeat: no-repeat; + background-clip: text; + -webkit-background-clip: text; + color: transparent; + animation: gradient-text-fill 680ms linear forwards; + background-image: linear-gradient( + to right, + var(--primary-color) 30%, + #fb275d 40%, + #00ca72 60%, + #ffcc00 85%, + transparent 90% + ); + } +} + +@keyframes fade { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes stroke-rotate { + to { + stroke-dashoffset: 0; + } +} + +@keyframes gradient-text-fill { + from { + background-position: 150% 0; + } + to { + background-position: 0% 0; + } +} diff --git a/packages/core/src/components/Label/LabelCelebrationAnimation.tsx b/packages/core/src/components/Label/LabelCelebrationAnimation.tsx new file mode 100644 index 0000000000..62759e8f31 --- /dev/null +++ b/packages/core/src/components/Label/LabelCelebrationAnimation.tsx @@ -0,0 +1,112 @@ +import React, { cloneElement, forwardRef, useCallback, useEffect, useRef, useState } from "react"; +import cx from "classnames"; +import useResizeObserver from "../../hooks/useResizeObserver"; +import styles from "./LabelCelebrationAnimation.module.scss"; + +const DEFAULT_BORDER_RADIUS = 4; +const DEFAULT_STROKE_WIDTH = 1; + +export interface LabelCelebrationAnimationProps { + children: React.ReactElement; + active: boolean; +} + +function LabelCelebrationAnimation({ children, active: isActive }: LabelCelebrationAnimationProps) { + const wrapperRef = useRef(); + const childRef = useRef(); + + const [active, setActive] = useState(isActive); + const [path, setPath] = useState(); + + useEffect(() => { + setActive(isActive); + }, [isActive]); + + const resizeObserverCallback = useCallback( + ({ borderBoxSize }: { borderBoxSize: { blockSize: number; inlineSize: number } }) => { + const { blockSize: height, inlineSize: width } = borderBoxSize || {}; + + if (wrapperRef.current && active) { + const d = getPath({ width, height }); + setPath(d); + + const perimeter = getPerimeter({ width, height }); + wrapperRef.current.style.setProperty("--container-perimeter", String(perimeter)); + } + }, + [active] + ); + + useResizeObserver({ + ref: wrapperRef, + callback: resizeObserverCallback, + debounceTime: 0 + }); + + const ChildComponentWithRef = forwardRef((_props, ref) => + cloneElement(children, { + ref + }) + ); + + if (!active) { + return children; + } + + return ( +
+ + + + + { + setActive(false); + }} + /> + + +
+ ); +} + +export default LabelCelebrationAnimation; + +function getPath({ + width, + height, + borderRadius = DEFAULT_BORDER_RADIUS, + strokeWidth = DEFAULT_STROKE_WIDTH +}: { + width: number; + height: number; + borderRadius?: number; + strokeWidth?: number; +}) { + return `M ${width - strokeWidth / 2}, ${borderRadius} V ${ + height - borderRadius + } A ${borderRadius} ${borderRadius} 0 0 1 ${width - borderRadius} ${ + height - strokeWidth / 2 + } H ${borderRadius} A ${borderRadius} ${borderRadius} 0 0 1 ${strokeWidth / 2} ${ + height - borderRadius + } V ${borderRadius} A ${borderRadius} ${borderRadius} 0 0 1 ${borderRadius} ${strokeWidth / 2} L ${ + width - borderRadius + }, ${strokeWidth / 2} A ${borderRadius} ${borderRadius} 0 0 1 ${width - strokeWidth / 2} ${borderRadius} Z`; +} + +function getPerimeter({ + width, + height, + borderRadius = DEFAULT_BORDER_RADIUS +}: { + width: number; + height: number; + borderRadius?: number; +}) { + const straightWidth = width - 2 * borderRadius; + const straightHeight = height - 2 * borderRadius; + const cornerCircumference = 2 * Math.PI * borderRadius; + return cornerCircumference + 2 * straightWidth + 2 * straightHeight; +} From 59a26754f86b2a554cd11dbdce67d947c202a217 Mon Sep 17 00:00:00 2001 From: Tal Koren Date: Mon, 25 Mar 2024 18:53:59 +0200 Subject: [PATCH 3/6] feat: label celebration animation --- packages/core/src/components/Label/Label.tsx | 48 +++++++++++++++---- .../LabelCelebrationAnimation.module.scss | 12 ++--- .../Label/LabelCelebrationAnimation.tsx | 43 ++++++----------- .../components/Label/__stories__/label.mdx | 8 ++++ .../Label/__stories__/label.stories.tsx | 25 ++++++++++ 5 files changed, 92 insertions(+), 44 deletions(-) diff --git a/packages/core/src/components/Label/Label.tsx b/packages/core/src/components/Label/Label.tsx index e5bced2c3e..cccf6abe01 100644 --- a/packages/core/src/components/Label/Label.tsx +++ b/packages/core/src/components/Label/Label.tsx @@ -2,7 +2,7 @@ import { camelCase } from "lodash-es"; import cx from "classnames"; import { ComponentDefaultTestId, getTestId } from "../../tests/test-ids-utils"; import { getStyle } from "../../helpers/typesciptCssModulesHelper"; -import React, { forwardRef, useCallback, useMemo, useRef } from "react"; +import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { backwardCompatibilityForProperties } from "../../helpers/backwardCompatibilityForProperties"; import Text from "../Text/Text"; import Leg from "./Leg"; @@ -28,7 +28,7 @@ export interface LabelProps extends VibeComponentProps { isAnimationDisabled?: boolean; isLegIncluded?: boolean; onClick?: (event: React.MouseEvent) => void; - celebration?: boolean; + celebrationAnimation?: boolean; } const Label: VibeComponent & { @@ -48,12 +48,13 @@ const Label: VibeComponent & { id, "data-testid": dataTestId, onClick, - celebration + celebrationAnimation }, ref ) => { const labelRef = useRef(null); const mergedRef = useMergeRef(ref, labelRef); + const [isCelebrationAnimation, setIsCelebrationAnimation] = useState(celebrationAnimation); const overrideClassName = backwardCompatibilityForProperties([className, wrapperClassName]) as string; const isClickable = Boolean(onClick); @@ -65,12 +66,13 @@ const Label: VibeComponent & { getStyle(styles, camelCase("kind" + "-" + kind)), getStyle(styles, camelCase("color" + "-" + color)), { - [styles.withAnimation]: !isAnimationDisabled && !celebration, + // When celebrationAnimation is active it wins over the default animation + [styles.withAnimation]: !isAnimationDisabled && !celebrationAnimation, [styles.withLeg]: isLegIncluded }, labelClassName ), - [kind, color, isAnimationDisabled, isLegIncluded, labelClassName, celebration] + [kind, color, isAnimationDisabled, isLegIncluded, labelClassName, celebrationAnimation] ); const onClickCallback = useCallback( @@ -94,8 +96,12 @@ const Label: VibeComponent & { labelRef ); - return ( - + useEffect(() => { + setIsCelebrationAnimation(celebrationAnimation); + }, [celebrationAnimation]); + + const label = useMemo(() => { + return ( & { type={Text.types.TEXT2} className={classNames} color={Text.colors.ON_INVERTED} - data-celebration-text={celebration} + data-celebration-text={celebrationAnimation} > {text} @@ -115,8 +121,30 @@ const Label: VibeComponent & { {isLegIncluded ? : null} - - ); + ); + }, [ + isClickable, + clickableProps, + overrideClassName, + dataTestId, + id, + mergedRef, + classNames, + celebrationAnimation, + text, + isLegIncluded + ]); + + // Celebration animation is applied only for line kind + if (isCelebrationAnimation && kind === "line") { + return ( + setIsCelebrationAnimation(false)}> + {label} + + ); + } + + return label; } ); diff --git a/packages/core/src/components/Label/LabelCelebrationAnimation.module.scss b/packages/core/src/components/Label/LabelCelebrationAnimation.module.scss index b769d79c05..759a87323e 100644 --- a/packages/core/src/components/Label/LabelCelebrationAnimation.module.scss +++ b/packages/core/src/components/Label/LabelCelebrationAnimation.module.scss @@ -20,7 +20,7 @@ stroke-dashoffset: var(--container-perimeter); &.base { - stroke: #ffcc00; + stroke: var(--color-egg_yolk); animation: fade 200ms linear forwards; animation-delay: 80ms; stroke-dasharray: 0; @@ -29,13 +29,13 @@ } &.first { - stroke: #00ca72; + stroke: var(--color-done-green); animation-delay: 80ms; animation-duration: 320ms; } &.second { - stroke: #fb275d; + stroke: var(--color-stuck-red); animation-delay: 200ms; animation-duration: 320ms; } @@ -58,9 +58,9 @@ background-image: linear-gradient( to right, var(--primary-color) 30%, - #fb275d 40%, - #00ca72 60%, - #ffcc00 85%, + var(--color-stuck-red) 40%, + var(--color-done-green) 60%, + var(--color-egg_yolk) 85%, transparent 90% ); } diff --git a/packages/core/src/components/Label/LabelCelebrationAnimation.tsx b/packages/core/src/components/Label/LabelCelebrationAnimation.tsx index 62759e8f31..eaa599eaff 100644 --- a/packages/core/src/components/Label/LabelCelebrationAnimation.tsx +++ b/packages/core/src/components/Label/LabelCelebrationAnimation.tsx @@ -1,4 +1,4 @@ -import React, { cloneElement, forwardRef, useCallback, useEffect, useRef, useState } from "react"; +import React, { cloneElement, forwardRef, useCallback, useRef, useState } from "react"; import cx from "classnames"; import useResizeObserver from "../../hooks/useResizeObserver"; import styles from "./LabelCelebrationAnimation.module.scss"; @@ -8,25 +8,20 @@ const DEFAULT_STROKE_WIDTH = 1; export interface LabelCelebrationAnimationProps { children: React.ReactElement; - active: boolean; + onAnimationEnd: () => void; } -function LabelCelebrationAnimation({ children, active: isActive }: LabelCelebrationAnimationProps) { +function LabelCelebrationAnimation({ children, onAnimationEnd }: LabelCelebrationAnimationProps) { const wrapperRef = useRef(); const childRef = useRef(); - const [active, setActive] = useState(isActive); const [path, setPath] = useState(); - useEffect(() => { - setActive(isActive); - }, [isActive]); - const resizeObserverCallback = useCallback( ({ borderBoxSize }: { borderBoxSize: { blockSize: number; inlineSize: number } }) => { const { blockSize: height, inlineSize: width } = borderBoxSize || {}; - if (wrapperRef.current && active) { + if (wrapperRef.current) { const d = getPath({ width, height }); setPath(d); @@ -34,7 +29,7 @@ function LabelCelebrationAnimation({ children, active: isActive }: LabelCelebrat wrapperRef.current.style.setProperty("--container-perimeter", String(perimeter)); } }, - [active] + [] ); useResizeObserver({ @@ -49,23 +44,13 @@ function LabelCelebrationAnimation({ children, active: isActive }: LabelCelebrat }) ); - if (!active) { - return children; - } - return (
- { - setActive(false); - }} - /> +
@@ -85,15 +70,17 @@ function getPath({ borderRadius?: number; strokeWidth?: number; }) { + const offset = strokeWidth / 2; + return `M ${width - strokeWidth / 2}, ${borderRadius} V ${ height - borderRadius - } A ${borderRadius} ${borderRadius} 0 0 1 ${width - borderRadius} ${ - height - strokeWidth / 2 - } H ${borderRadius} A ${borderRadius} ${borderRadius} 0 0 1 ${strokeWidth / 2} ${ - height - borderRadius - } V ${borderRadius} A ${borderRadius} ${borderRadius} 0 0 1 ${borderRadius} ${strokeWidth / 2} L ${ - width - borderRadius - }, ${strokeWidth / 2} A ${borderRadius} ${borderRadius} 0 0 1 ${width - strokeWidth / 2} ${borderRadius} Z`; + } A ${borderRadius} ${borderRadius} 0 0 1 ${width - borderRadius} ${height - strokeWidth / 2} H ${ + borderRadius + offset + } A ${borderRadius} ${borderRadius} 0 0 1 ${strokeWidth / 2} ${height - borderRadius} V ${ + borderRadius + offset + } A ${borderRadius} ${borderRadius} 0 0 1 ${borderRadius} ${strokeWidth / 2} L ${width - borderRadius}, ${ + strokeWidth / 2 + } A ${borderRadius} ${borderRadius} 0 0 1 ${width - strokeWidth / 2} ${borderRadius} Z`; } function getPerimeter({ diff --git a/packages/core/src/components/Label/__stories__/label.mdx b/packages/core/src/components/Label/__stories__/label.mdx index 2baf49f734..daea2b5ec0 100644 --- a/packages/core/src/components/Label/__stories__/label.mdx +++ b/packages/core/src/components/Label/__stories__/label.mdx @@ -144,6 +144,14 @@ In case of visual overload, use the secondary label in order to create hirarchy. +### Celebration + +To celebrate new feature, outline label can be highlighted by adding celebrate animation. + + + + + ## Related components diff --git a/packages/core/src/components/Label/__stories__/label.stories.tsx b/packages/core/src/components/Label/__stories__/label.stories.tsx index 7af9b0a4d7..a997ace55d 100644 --- a/packages/core/src/components/Label/__stories__/label.stories.tsx +++ b/packages/core/src/components/Label/__stories__/label.stories.tsx @@ -3,6 +3,7 @@ import { createStoryMetaSettingsDecorator } from "../../../storybook"; import { NOOP } from "../../../utils/function-utils"; import { createComponentTemplate, MultipleStoryElementsWrapper } from "vibe-storybook-components"; import "./label.stories.scss"; +import { useEffect, useState } from "react"; const metaSettings = createStoryMetaSettingsDecorator({ component: Label, @@ -130,3 +131,27 @@ export const SecondaryLabel = { } } }; + +export const Celebration = { + render: () => { + const [animate, setAnimate] = useState(false); + + useEffect(() => { + setTimeout(() => { + setAnimate(false); + }, 500); + }, [animate]); + + return ( + <> +