diff --git a/.changeset/sweet-news-search.md b/.changeset/sweet-news-search.md new file mode 100644 index 00000000000..38beaf67119 --- /dev/null +++ b/.changeset/sweet-news-search.md @@ -0,0 +1,10 @@ +--- +'@kaizen/components': patch +--- + +Add optional prop to `VideoPlayer` and update `BrandMomentCaptureIntro` to always display the animation toggle. + +- Add `hasVisibleAnimationToggle` prop to `VideoPlayer` +- Update VideoPlayer to next `Button` component +- Remove redundant styles from button migration +- Add i18n strings for play/pause labels diff --git a/packages/components/locales/en.json b/packages/components/locales/en.json index b18eaf5985a..36e210cd2c7 100644 --- a/packages/components/locales/en.json +++ b/packages/components/locales/en.json @@ -188,5 +188,13 @@ "splitButton.dropdownButton.label": { "description": "Label for a dropdown menu holding additional actions", "message": "Additional actions" + }, + "videoPlayer.pausePlayBtn.pauseLabel": { + "description": "Label for the pausing / stopping an animation", + "message": "Play animation" + }, + "videoPlayer.pausePlayBtn.playLabel": { + "description": "Label for the starting / playing an animation", + "message": "Play animation" } } diff --git a/packages/components/src/Illustration/Scene/BrandMomentCaptureIntro/BrandMomentCaptureIntro.tsx b/packages/components/src/Illustration/Scene/BrandMomentCaptureIntro/BrandMomentCaptureIntro.tsx index 2ca1da82f98..fada50176dc 100644 --- a/packages/components/src/Illustration/Scene/BrandMomentCaptureIntro/BrandMomentCaptureIntro.tsx +++ b/packages/components/src/Illustration/Scene/BrandMomentCaptureIntro/BrandMomentCaptureIntro.tsx @@ -12,10 +12,13 @@ export const BrandMomentCaptureIntro = ({ isAnimated, alt, enableAspectRatio, + autoplay, + loop, ...otherProps }: AnimatedSceneProps): JSX.Element => { const [firstAnimationComplete, setFirstAnimationComplete] = useState(false) const aspectRatio = enableAspectRatio ? 'landscape' : undefined + const shouldAlwaysShowAnimationToggle = autoplay && loop if (!isAnimated) { return ( @@ -35,7 +38,9 @@ export const BrandMomentCaptureIntro = ({ aspectRatio={aspectRatio} fallback="illustrations/heart/scene/brand-moments-capture-intro-loop" source="illustrations/heart/scene/brand-moments-capture-intro-loop" - autoplay={firstAnimationComplete ? otherProps.autoplay : false} + autoplay={firstAnimationComplete ? autoplay : false} + loop={loop} + hasVisibleAnimationToggle={shouldAlwaysShowAnimationToggle} /> ) } @@ -47,7 +52,9 @@ export const BrandMomentCaptureIntro = ({ fallback="illustrations/heart/scene/brand-moments-capture-intro" source="illustrations/heart/scene/brand-moments-capture-intro" onEnded={(): void => setFirstAnimationComplete(true)} + autoplay={autoplay} loop={false} + hasVisibleAnimationToggle={shouldAlwaysShowAnimationToggle} /> ) } diff --git a/packages/components/src/Illustration/Scene/BrandMomentCaptureIntro/_docs/BrandMomentCaptureIntro.mdx b/packages/components/src/Illustration/Scene/BrandMomentCaptureIntro/_docs/BrandMomentCaptureIntro.mdx index e6fcdd98c9f..e14c4d2a8d7 100644 --- a/packages/components/src/Illustration/Scene/BrandMomentCaptureIntro/_docs/BrandMomentCaptureIntro.mdx +++ b/packages/components/src/Illustration/Scene/BrandMomentCaptureIntro/_docs/BrandMomentCaptureIntro.mdx @@ -45,3 +45,9 @@ Will render the a looped animation. Should be used with `isAnimated` to render t If false will render the animation paused. This can be re-enabled clicking the pause sign on hover. + +## Example + +This is not an exact template, rather an example of the component in use with a looping animation. + + diff --git a/packages/components/src/Illustration/Scene/BrandMomentCaptureIntro/_docs/BrandMomentCaptureIntro.stories.tsx b/packages/components/src/Illustration/Scene/BrandMomentCaptureIntro/_docs/BrandMomentCaptureIntro.stories.tsx index 0917f224399..49b7ba40133 100644 --- a/packages/components/src/Illustration/Scene/BrandMomentCaptureIntro/_docs/BrandMomentCaptureIntro.stories.tsx +++ b/packages/components/src/Illustration/Scene/BrandMomentCaptureIntro/_docs/BrandMomentCaptureIntro.stories.tsx @@ -1,4 +1,9 @@ +import React from 'react' import { type Meta, type StoryObj } from '@storybook/react' +import { Heading } from '~components/Heading' +import { Link } from '~components/Link' +import { Text } from '~components/Text' +import { Button, Icon } from '~components/__next__' import { BrandMomentCaptureIntro } from '../index' const meta = { @@ -21,6 +26,7 @@ export const Playground: Story = { } export const Animated: Story = { + name: 'Animated Capture illustration on toggle', args: { loop: false, isAnimated: true, @@ -28,6 +34,7 @@ export const Animated: Story = { } export const Looped: Story = { + name: 'Animated Capture illustration looped without autoplay', args: { isAnimated: true, loop: true, @@ -35,9 +42,46 @@ export const Looped: Story = { } export const Autoplay: Story = { + name: 'Animated Capture illustration looped with autoplay', args: { isAnimated: true, loop: true, - autoplay: false, + autoplay: true, }, } + +// This is an example that provides a closer representation of how the component is used in the product. +export const CaptureExample: Story = { + args: { + isAnimated: true, + loop: true, + autoplay: true, + }, + render: (args) => ( +
+
+ +
+
+ + Survey Title + + + You have been asked to provide feedback for Demonstration Employee. + + + The setting for this survey control how your responses can be used by Hooli. + + + Your information will be stored and processed in accordance with Culture Amp’s{' '} + Privacy Policy. More on managing information. + +
+ +
+
+
+ ), +} diff --git a/packages/components/src/Illustration/Scene/Scene.tsx b/packages/components/src/Illustration/Scene/Scene.tsx index 6ea4866305d..7a5fc5fe2c9 100644 --- a/packages/components/src/Illustration/Scene/Scene.tsx +++ b/packages/components/src/Illustration/Scene/Scene.tsx @@ -10,7 +10,7 @@ export type SceneProps = { type BaseAnimatedSceneProps = { isAnimated?: boolean } & SceneProps & - Pick + Pick type AnimatedProps = { isAnimated: true @@ -21,6 +21,7 @@ type NotAnimatedProps = { isAnimated?: false autoplay?: never loop?: never + hasVisibleAnimationToggle?: never } & BaseAnimatedSceneProps export type AnimatedSceneProps = AnimatedProps | NotAnimatedProps diff --git a/packages/components/src/Illustration/Scene/_docs/Scene.mdx b/packages/components/src/Illustration/Scene/_docs/Scene.mdx index 5f24a922fdb..23f95573264 100644 --- a/packages/components/src/Illustration/Scene/_docs/Scene.mdx +++ b/packages/components/src/Illustration/Scene/_docs/Scene.mdx @@ -20,3 +20,13 @@ import * as SceneStories from './Scene.stories' + +### Animated scenes + +Some Scene illustrations have animated versions. Looped animation should be used sparingly as it can be distracting for users, especially those with motion sensitivity. Consider also the WCAG spec for [non-essential animations](https://www.w3.org/WAI/WCAG22/Understanding/animation-from-interactions.html). + +Note that all Scene illustration with `autoplay` and `loop` set to `true` will render the animation toggle. + +This can be overridden by setting `hasVisibleAnimationToggle` to `false`, which will render the toggle only on hover or focus. Note that this can have accessibility implications, so use with caution. + + diff --git a/packages/components/src/Illustration/Scene/_docs/Scene.stories.tsx b/packages/components/src/Illustration/Scene/_docs/Scene.stories.tsx index b30b737a7df..586d810145f 100644 --- a/packages/components/src/Illustration/Scene/_docs/Scene.stories.tsx +++ b/packages/components/src/Illustration/Scene/_docs/Scene.stories.tsx @@ -1,9 +1,19 @@ +import React from 'react' import { type Meta, type StoryObj } from '@storybook/react' -import { BrandMomentPositiveOutro } from '../index' +import { + BrandMomentError, + BrandMomentLogin, + BrandMomentPositiveOutro, + EmptyStatesAction, + EmptyStatesInformative, + EmptyStatesNegative, + EmptyStatesNeutral, + EmptyStatesPositive, +} from '../index' const meta = { - title: 'Components/Illustrations/Scene', + title: 'Components/illustrations/Scene', component: BrandMomentPositiveOutro, } satisfies Meta @@ -20,3 +30,69 @@ export const Playground: Story = { }, }, } + +export const AnimatedScenes: Story = { + name: 'Animated Scene illustration autoplay and loop', + args: { + isAnimated: true, + loop: true, + autoplay: true, + }, + render: (args) => ( + <> + + + + + + + + + + ), + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} +export const AnimatedScenesWithAutoPlayNoLoop: Story = { + name: 'Animated Scene illustration with autoplay but no loop', + ...AnimatedScenes, + args: { + isAnimated: true, + loop: false, + autoplay: true, + }, +} + +export const AnimatedScenesLoopedWithoutAutoplay: Story = { + name: 'Animated Scene illustration without autoplay but with loop', + ...AnimatedScenes, + args: { + isAnimated: true, + loop: true, + autoplay: false, + }, +} + +export const AnimatedScenesHover: Story = { + ...AnimatedScenes, + parameters: { + pseudo: { + hover: ['[data-sb-pseudo-styles="hover"]', '[data-sb-pseudo-styles="hover"] figure'], + }, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} diff --git a/packages/components/src/Illustration/subcomponents/Base/Base.module.css b/packages/components/src/Illustration/subcomponents/Base/Base.module.css new file mode 100644 index 00000000000..db09a7d61cf --- /dev/null +++ b/packages/components/src/Illustration/subcomponents/Base/Base.module.css @@ -0,0 +1,97 @@ +@layer kz-components { + .wrapper { + width: 100%; + margin: 0; + } + + .figure { + margin: 0; + position: relative; + } + + .figure .pausePlayButton { + opacity: 0; + position: absolute; + left: 0; + bottom: 0; + border-width: 1px; + transition: all var(--animation-duration-immediate) ease; + + @media (hover: none) and (pointer: coarse) { + background: var(--color-gray-200); + opacity: 1; + } + } + + .figure .pausePlayButton:hover, + .figure .pausePlayButton:focus, + .figure:hover .pausePlayButton { + opacity: 1; + background: var(--color-gray-200); + border-color: var(--color-gray-600); + } + + .hasVisibleAnimationToggle { + opacity: 1 !important; + } + + .aspectRatioWrapper { + margin: 0; + display: flex; + align-items: center; + } + + .landscape { + aspect-ratio: 4/3; + + @supports not (aspect-ratio: auto) { + &::before { + float: left; + padding-top: 75%; + content: ''; + } + + &::after { + display: block; + content: ''; + clear: both; + } + } + } + + .portrait { + aspect-ratio: 3/4; + + @supports not (aspect-ratio: auto) { + &::before { + float: left; + padding-top: 133.33%; + content: ''; + } + + &::after { + display: block; + content: ''; + clear: both; + } + } + } + + .square { + aspect-ratio: auto 1/1; + + @supports not (aspect-ratio: auto) { + &::before { + float: left; + padding-top: 100%; + content: ''; + } + + &::after { + display: block; + content: ''; + clear: both; + } + } + } +} diff --git a/packages/components/src/Illustration/subcomponents/Base/Base.module.scss b/packages/components/src/Illustration/subcomponents/Base/Base.module.scss deleted file mode 100644 index 509eef1c0c4..00000000000 --- a/packages/components/src/Illustration/subcomponents/Base/Base.module.scss +++ /dev/null @@ -1,127 +0,0 @@ -@import '~@kaizen/design-tokens/sass/color'; -@import '~@kaizen/design-tokens/sass/animation'; - -@layer kz-components { - .wrapper { - width: 100%; - margin: 0; - } - - .figure { - margin: 0; - position: relative; - } - - // nested to get more specificity, beat out the generic button styles - .figure .pausePlayButton { - opacity: 0%; - position: absolute; - right: 1rem; - bottom: 1rem; - /* stylelint-disable declaration-no-important */ - background-color: $color-white !important; - border: 1px solid $color-gray-400; - transition: all $animation-duration-immediate; - - @media (hover: none) and (pointer: coarse) { - opacity: 100%; - } - - svg { - color: $color-purple-800; - opacity: 70%; - } - - &:hover, - &:focus { - opacity: 100%; - background-color: $color-gray-200 !important; - - svg { - opacity: 100%; - } - } - } - - .figure:hover { - .pausePlayButton { - opacity: 100%; - } - } - - .aspectRatioWrapper { - margin: 0; - display: flex; - align-items: center; - } - - .landscape { - aspect-ratio: 4/3; - - @supports not (aspect-ratio: auto) { - &::before { - float: left; - padding-top: 75%; - content: ''; - } - - &::after { - display: block; - content: ''; - clear: both; - } - } - } - - .portrait { - aspect-ratio: 3/4; - - @supports not (aspect-ratio: auto) { - &::before { - float: left; - padding-top: 133.33%; - content: ''; - } - - &::after { - display: block; - content: ''; - clear: both; - } - } - } - - .square { - aspect-ratio: auto 1/1; - - @supports not (aspect-ratio: auto) { - &::before { - float: left; - padding-top: 100%; - content: ''; - } - - &::after { - display: block; - content: ''; - clear: both; - } - } - } - - // If the .visually-hidden class is applied to natively focusable elements - // (such as a, button, input, etc) they must become visible when they receive - // keyboard focus. Otherwise, a sighted keyboard user would have to try and - // figure out where their visible focus indicator had gone to. - .visuallyHidden:not(:focus, :active) { - clip: rect(0, 0, 0, 0); - clip-path: inset(50%); - position: absolute; - width: 1px; - height: 1px; - padding: 0; - overflow: hidden; - white-space: nowrap; - border: 0; - } -} diff --git a/packages/components/src/Illustration/subcomponents/Base/Base.tsx b/packages/components/src/Illustration/subcomponents/Base/Base.tsx index 2c86079d79f..7c614cb69e1 100644 --- a/packages/components/src/Illustration/subcomponents/Base/Base.tsx +++ b/packages/components/src/Illustration/subcomponents/Base/Base.tsx @@ -2,7 +2,7 @@ import React, { type HTMLAttributes } from 'react' import classnames from 'classnames' import { type OverrideClassName } from '~components/types/OverrideClassName' import { assetUrl } from '~components/utils/hostedAssets' -import styles from './Base.module.scss' +import styles from './Base.module.css' export type BaseProps = { /** diff --git a/packages/components/src/Illustration/subcomponents/VideoPlayer/VideoPlayer.tsx b/packages/components/src/Illustration/subcomponents/VideoPlayer/VideoPlayer.tsx index 33c4478ceb7..6ee76ddbac0 100644 --- a/packages/components/src/Illustration/subcomponents/VideoPlayer/VideoPlayer.tsx +++ b/packages/components/src/Illustration/subcomponents/VideoPlayer/VideoPlayer.tsx @@ -1,10 +1,11 @@ import React, { useEffect, useRef } from 'react' import classnames from 'classnames' -import { IconButton } from '~components/Button' +import { VisuallyHidden } from '~components/VisuallyHidden' +import { Button } from '~components/__next__' import { assetUrl } from '~components/utils/hostedAssets' import { canPlayWebm } from '../../utils/canPlayWebm' import { usePausePlay } from '../../utils/usePausePlay' -import styles from '../Base/Base.module.scss' +import styles from '../Base/Base.module.css' export type VideoPlayerProps = { /** @@ -37,12 +38,17 @@ export type VideoPlayerProps = { */ aspectRatio?: 'landscape' | 'portrait' | 'square' + /** + * Controls whether the animation toggle is always visible or will only be visible on hover. This will supersede the default behavior of always being visible if `loop` and `autoplay` are `true` - note that this can have accessibility implications. @default undefined + */ + hasVisibleAnimationToggle?: boolean onEnded?: () => void } export const VideoPlayer = ({ autoplay = true, loop = false, + hasVisibleAnimationToggle, fallback, source, aspectRatio, @@ -155,6 +161,7 @@ export const VideoPlayer = ({ }, [windowIsAvailable]) const pausePlay = usePausePlay(videoRef) + const shouldAlwaysShowAnimationToggle = hasVisibleAnimationToggle ?? (autoplay && loop) return (
pausePlay.toggle()} > {isWebmCompatible && } - pausePlay.toggle()} +
) } diff --git a/packages/components/src/Illustration/utils/usePausePlay.tsx b/packages/components/src/Illustration/utils/usePausePlay.tsx index 9a1b3e36f66..21805094ea3 100644 --- a/packages/components/src/Illustration/utils/usePausePlay.tsx +++ b/packages/components/src/Illustration/utils/usePausePlay.tsx @@ -1,4 +1,5 @@ -import React, { useState, type RefObject } from 'react' +import React, { useEffect, useState, type RefObject } from 'react' +import { useIntl } from '@cultureamp/i18n-react-intl' import { Icon } from '~components/__next__/Icon' export type usePausePlayHook = { @@ -8,21 +9,50 @@ export type usePausePlayHook = { } export const usePausePlay = (videoRef: RefObject): usePausePlayHook => { - const [isPaused, setPaused] = useState(false) + const { formatMessage } = useIntl() + const [isPaused, setPaused] = useState(true) + + useEffect(() => { + const video = videoRef.current + if (!video) return + + setPaused(video.paused) + + const handlePlay = (): void => setPaused(false) + const handlePause = (): void => setPaused(true) + + video.addEventListener('play', handlePlay) + video.addEventListener('pause', handlePause) + + return () => { + video.removeEventListener('play', handlePlay) + video.removeEventListener('pause', handlePause) + } + }, [videoRef]) + + const playAnimationLabel = formatMessage({ + id: 'videoPlayer.pausePlayBtn.playLabel', + defaultMessage: 'Play animation', + description: 'Label for the starting / playing an animation', + }) + + const pauseAnimationLabel = formatMessage({ + id: 'videoPlayer.pausePlayBtn.pauseLabel', + defaultMessage: 'Pause animation', + description: 'Label for the pausing / stopping an animation', + }) return { toggle: (): void => { if (!videoRef.current) return if (videoRef.current.paused) { - setPaused(false) videoRef.current.play() } else { - setPaused(true) videoRef.current.pause() } }, - icon: , - label: isPaused ? 'Play animation' : 'Pause animation', + icon: , + label: isPaused ? playAnimationLabel : pauseAnimationLabel, } }