diff --git a/playroom/snippets.tsx b/playroom/snippets.tsx index e68f6c4799..9f09b6d108 100644 --- a/playroom/snippets.tsx +++ b/playroom/snippets.tsx @@ -2833,11 +2833,73 @@ const ProgressBlockSnippets = [ }, ]; +const timerSnippets: Array = [ + { + group: 'TextTimer', + name: 'No Label', + code: ` + + `, + }, + { + group: 'TextTimer', + name: 'Short Label', + code: ` + + `, + }, + { + group: 'TextTimer', + name: 'Long Label', + code: ` + + `, + }, + { + group: 'Timer', + name: 'default', + code: ` + + `, + }, + { + group: 'Timer', + name: 'boxed', + code: ` + + `, + }, +]; + export default [ ...buttonSnippets, ...formSnippets, ...feedbackSnippets, ...skeletonSnippets, + ...timerSnippets, {group: 'Feedbacks', name: 'Snackbar', code: ''}, ...layoutSnippets, { diff --git a/src/__acceptance_tests__/__ssr_pages__/timer.tsx b/src/__acceptance_tests__/__ssr_pages__/timer.tsx new file mode 100644 index 0000000000..6d9c60de66 --- /dev/null +++ b/src/__acceptance_tests__/__ssr_pages__/timer.tsx @@ -0,0 +1,6 @@ +import * as React from 'react'; +import {Timer} from '../../..'; + +const TimerTest = (): JSX.Element => ; + +export default TimerTest; diff --git a/src/__acceptance_tests__/timer-ssr-acceptance-test.tsx b/src/__acceptance_tests__/timer-ssr-acceptance-test.tsx new file mode 100644 index 0000000000..bd0ac43ba1 --- /dev/null +++ b/src/__acceptance_tests__/timer-ssr-acceptance-test.tsx @@ -0,0 +1,5 @@ +import {openSSRPage} from '../test-utils'; + +test('ssr timer', async () => { + await openSSRPage({name: 'timer'}); +}); diff --git a/src/__private_stories__/skin-components-story.tsx b/src/__private_stories__/skin-components-story.tsx index f1a4927fd4..ff782b5479 100644 --- a/src/__private_stories__/skin-components-story.tsx +++ b/src/__private_stories__/skin-components-story.tsx @@ -43,6 +43,7 @@ import { Title3, IconButton, Hero, + Timer, } from '..'; import {InternalIconButton} from '../icon-button'; import avatarImg from '../__stories__/images/avatar.jpg'; @@ -396,6 +397,12 @@ export const Default: StoryComponent = ({variant}) => { button={ {}}>Action} buttonLink={ {}}>Link} /> + + {/** Timer */} + + + + diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-blau-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-blau-1-snap.png index 04ee34e75b..075b63a142 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-blau-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-blau-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-blau-alternative-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-blau-alternative-1-snap.png index 028aa25d17..1821bf159a 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-blau-alternative-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-blau-alternative-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-blau-dark-mode-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-blau-dark-mode-1-snap.png index d249fc4c65..1f51276a7b 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-blau-dark-mode-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-blau-dark-mode-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-blau-inverse-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-blau-inverse-1-snap.png index f8a82ea869..aaac00cbcf 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-blau-inverse-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-blau-inverse-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-blau-inverse-and-dark-mode-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-blau-inverse-and-dark-mode-1-snap.png index afbe3658d7..719686ba4a 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-blau-inverse-and-dark-mode-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-blau-inverse-and-dark-mode-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-movistar-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-movistar-1-snap.png index df03ae2389..69237dbf89 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-movistar-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-movistar-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-movistar-alternative-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-movistar-alternative-1-snap.png index 4f75ca78ad..4690f31081 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-movistar-alternative-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-movistar-alternative-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-movistar-dark-mode-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-movistar-dark-mode-1-snap.png index 6de24123e2..aba1b70ce0 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-movistar-dark-mode-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-movistar-dark-mode-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-movistar-inverse-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-movistar-inverse-1-snap.png index a76bbe8c7b..cfa14c25f3 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-movistar-inverse-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-movistar-inverse-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-movistar-inverse-and-dark-mode-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-movistar-inverse-and-dark-mode-1-snap.png index 5e9ea4c950..b1e90c95bd 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-movistar-inverse-and-dark-mode-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-movistar-inverse-and-dark-mode-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-1-snap.png index 5a6b84a708..a3759ba42e 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-alternative-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-alternative-1-snap.png index 8f541833c8..0548f86bd6 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-alternative-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-alternative-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-dark-mode-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-dark-mode-1-snap.png index 5cfb627138..55fad8788a 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-dark-mode-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-dark-mode-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-inverse-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-inverse-1-snap.png index b90a4a80a3..9456116e00 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-inverse-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-inverse-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-inverse-and-dark-mode-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-inverse-and-dark-mode-1-snap.png index de87a32ef0..d9fd5fea7e 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-inverse-and-dark-mode-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-inverse-and-dark-mode-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-new-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-new-1-snap.png index deafac1a08..2af03c0904 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-new-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-new-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-new-alternative-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-new-alternative-1-snap.png index 0c91dd5902..2f58c8b1b6 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-new-alternative-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-new-alternative-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-new-dark-mode-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-new-dark-mode-1-snap.png index 49e54aeace..daf785dc2f 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-new-dark-mode-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-new-dark-mode-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-new-inverse-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-new-inverse-1-snap.png index 4bacee473f..0d39b390b4 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-new-inverse-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-new-inverse-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-new-inverse-and-dark-mode-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-new-inverse-and-dark-mode-1-snap.png index 1ef0c41585..4a0a533366 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-new-inverse-and-dark-mode-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-o-2-new-inverse-and-dark-mode-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-telefonica-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-telefonica-1-snap.png index f5247bc7f9..c6bcebcbf6 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-telefonica-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-telefonica-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-telefonica-alternative-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-telefonica-alternative-1-snap.png index dbacf08f63..48052b9970 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-telefonica-alternative-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-telefonica-alternative-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-telefonica-dark-mode-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-telefonica-dark-mode-1-snap.png index fcaf795ed9..4584ca2cec 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-telefonica-dark-mode-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-telefonica-dark-mode-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-telefonica-inverse-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-telefonica-inverse-1-snap.png index 5976c11249..f7f2268afd 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-telefonica-inverse-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-telefonica-inverse-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-telefonica-inverse-and-dark-mode-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-telefonica-inverse-and-dark-mode-1-snap.png index b38ba2414d..f600aa7155 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-telefonica-inverse-and-dark-mode-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-telefonica-inverse-and-dark-mode-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-tu-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-tu-1-snap.png index 549a71c98d..e1f5327333 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-tu-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-tu-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-tu-alternative-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-tu-alternative-1-snap.png index 612a4b2c58..febf4fd3d4 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-tu-alternative-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-tu-alternative-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-tu-dark-mode-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-tu-dark-mode-1-snap.png index 1b77d02ccf..666292634c 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-tu-dark-mode-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-tu-dark-mode-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-tu-inverse-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-tu-inverse-1-snap.png index efb15dd814..7c40bbbe86 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-tu-inverse-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-tu-inverse-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-tu-inverse-and-dark-mode-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-tu-inverse-and-dark-mode-1-snap.png index 95586cccaa..404d120e52 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-tu-inverse-and-dark-mode-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-tu-inverse-and-dark-mode-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-vivo-new-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-vivo-new-1-snap.png index 8eb0d18f52..1697e2a1e7 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-vivo-new-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-vivo-new-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-vivo-new-alternative-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-vivo-new-alternative-1-snap.png index 10a4623c57..d1e90ee952 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-vivo-new-alternative-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-vivo-new-alternative-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-vivo-new-dark-mode-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-vivo-new-dark-mode-1-snap.png index b74ff44f41..ef9ce713b4 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-vivo-new-dark-mode-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-vivo-new-dark-mode-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-vivo-new-inverse-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-vivo-new-inverse-1-snap.png index 24719605c9..7e957c583e 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-vivo-new-inverse-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-vivo-new-inverse-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-vivo-new-inverse-and-dark-mode-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-vivo-new-inverse-and-dark-mode-1-snap.png index a19211fc33..cc8d1bac03 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-vivo-new-inverse-and-dark-mode-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/private-skin-components-screenshot-test-tsx-components-in-vivo-new-inverse-and-dark-mode-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-text-timer-min-time-unit-minutes-max-time-unit-days-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-text-timer-min-time-unit-minutes-max-time-unit-days-1-snap.png new file mode 100644 index 0000000000..cf42adf078 Binary files /dev/null and b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-text-timer-min-time-unit-minutes-max-time-unit-days-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-text-timer-min-time-unit-minutes-max-time-unit-days-2-snap.png b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-text-timer-min-time-unit-minutes-max-time-unit-days-2-snap.png new file mode 100644 index 0000000000..cf42adf078 Binary files /dev/null and b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-text-timer-min-time-unit-minutes-max-time-unit-days-2-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-text-timer-min-time-unit-seconds-max-time-unit-days-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-text-timer-min-time-unit-seconds-max-time-unit-days-1-snap.png new file mode 100644 index 0000000000..d2c364ab0c Binary files /dev/null and b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-text-timer-min-time-unit-seconds-max-time-unit-days-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-text-timer-min-time-unit-seconds-max-time-unit-hours-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-text-timer-min-time-unit-seconds-max-time-unit-hours-1-snap.png new file mode 100644 index 0000000000..d2c364ab0c Binary files /dev/null and b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-text-timer-min-time-unit-seconds-max-time-unit-hours-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-text-timer-min-time-unit-seconds-max-time-unit-seconds-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-text-timer-min-time-unit-seconds-max-time-unit-seconds-1-snap.png new file mode 100644 index 0000000000..373785f1d0 Binary files /dev/null and b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-text-timer-min-time-unit-seconds-max-time-unit-seconds-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-timer-boxed-desktop-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-timer-boxed-desktop-1-snap.png new file mode 100644 index 0000000000..0a3f3980b7 Binary files /dev/null and b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-timer-boxed-desktop-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-timer-boxed-mobile-ios-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-timer-boxed-mobile-ios-1-snap.png new file mode 100644 index 0000000000..702b743e49 Binary files /dev/null and b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-timer-boxed-mobile-ios-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-timer-boxed-with-big-font-size-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-timer-boxed-with-big-font-size-1-snap.png new file mode 100644 index 0000000000..fc97ea599c Binary files /dev/null and b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-timer-boxed-with-big-font-size-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-timer-min-time-unit-minutes-max-time-unit-days-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-timer-min-time-unit-minutes-max-time-unit-days-1-snap.png new file mode 100644 index 0000000000..984d268ebb Binary files /dev/null and b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-timer-min-time-unit-minutes-max-time-unit-days-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-timer-min-time-unit-minutes-max-time-unit-days-2-snap.png b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-timer-min-time-unit-minutes-max-time-unit-days-2-snap.png new file mode 100644 index 0000000000..984d268ebb Binary files /dev/null and b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-timer-min-time-unit-minutes-max-time-unit-days-2-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-timer-min-time-unit-seconds-max-time-unit-days-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-timer-min-time-unit-seconds-max-time-unit-days-1-snap.png new file mode 100644 index 0000000000..f46da82347 Binary files /dev/null and b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-timer-min-time-unit-seconds-max-time-unit-days-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-timer-min-time-unit-seconds-max-time-unit-hours-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-timer-min-time-unit-seconds-max-time-unit-hours-1-snap.png new file mode 100644 index 0000000000..0e0c020a9a Binary files /dev/null and b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-timer-min-time-unit-seconds-max-time-unit-hours-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-timer-min-time-unit-seconds-max-time-unit-seconds-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-timer-min-time-unit-seconds-max-time-unit-seconds-1-snap.png new file mode 100644 index 0000000000..1510b11c6c Binary files /dev/null and b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-timer-min-time-unit-seconds-max-time-unit-seconds-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-timer-wraps-if-needed-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-timer-wraps-if-needed-1-snap.png new file mode 100644 index 0000000000..4385d95c81 Binary files /dev/null and b/src/__screenshot_tests__/__image_snapshots__/timer-screenshot-test-tsx-timer-wraps-if-needed-1-snap.png differ diff --git a/src/__screenshot_tests__/timer-screenshot-test.tsx b/src/__screenshot_tests__/timer-screenshot-test.tsx new file mode 100644 index 0000000000..8391caf992 --- /dev/null +++ b/src/__screenshot_tests__/timer-screenshot-test.tsx @@ -0,0 +1,85 @@ +import {openStoryPage, screen, setRootFontSize} from '../test-utils'; + +const DEVICE = ['DESKTOP', 'MOBILE_IOS'] as const; + +test.each` + minTimeUnit | maxTimeUnit + ${'seconds'} | ${'hours'} + ${'minutes'} | ${'days'} + ${'seconds'} | ${'days'} + ${'minutes'} | ${'days'} + ${'seconds'} | ${'seconds'} +`( + 'TextTimer - minTimeUnit = $minTimeUnit, maxTimeUnit = $maxTimeUnit', + async ({minTimeUnit, maxTimeUnit}) => { + await openStoryPage({ + id: 'components-timer--text-timer-story', + args: {minTimeUnit, maxTimeUnit}, + }); + + const timer = await screen.findByTestId('timer'); + + const image = await timer.screenshot(); + expect(image).toMatchImageSnapshot(); + } +); + +test.each` + minTimeUnit | maxTimeUnit + ${'seconds'} | ${'hours'} + ${'minutes'} | ${'days'} + ${'seconds'} | ${'days'} + ${'minutes'} | ${'days'} + ${'seconds'} | ${'seconds'} +`('Timer - minTimeUnit = $minTimeUnit, maxTimeUnit = $maxTimeUnit', async ({minTimeUnit, maxTimeUnit}) => { + await openStoryPage({ + id: 'components-timer--timer-story', + args: {minTimeUnit, maxTimeUnit}, + }); + + const timer = await screen.findByTestId('timer'); + + const image = await timer.screenshot(); + expect(image).toMatchImageSnapshot(); +}); + +test.each(DEVICE)('Timer - boxed (%s)', async (device) => { + await openStoryPage({ + id: 'components-timer--timer-story', + device, + args: {minTimeUnit: 'seconds', maxTimeUnit: 'days', boxed: true}, + }); + + const timer = await screen.findByTestId('timer'); + + const image = await timer.screenshot(); + expect(image).toMatchImageSnapshot(); +}); + +test('Timer - boxed with big fontSize', async () => { + await openStoryPage({ + id: 'components-timer--timer-story', + args: {minTimeUnit: 'seconds', maxTimeUnit: 'days', boxed: true}, + }); + + await setRootFontSize(32); + + const timer = await screen.findByTestId('timer'); + + const image = await timer.screenshot(); + expect(image).toMatchImageSnapshot(); +}); + +test('Timer - wraps if needed', async () => { + await openStoryPage({ + id: 'components-timer--timer-story', + args: {minTimeUnit: 'seconds', maxTimeUnit: 'days', boxed: true}, + }); + + await setRootFontSize(72); + + const timer = await screen.findByTestId('timer'); + + const image = await timer.screenshot(); + expect(image).toMatchImageSnapshot(); +}); diff --git a/src/__stories__/timer-story.tsx b/src/__stories__/timer-story.tsx new file mode 100644 index 0000000000..f76a1102a9 --- /dev/null +++ b/src/__stories__/timer-story.tsx @@ -0,0 +1,179 @@ +import * as React from 'react'; +import {ResponsiveLayout, Box, Text3, Timer, TextTimer, Stack, Title1, Text2} from '..'; +import {isEqual} from '../utils/helpers'; + +import type {RemainingTime, TimeUnit} from '../timer'; +import type {Variant} from '../theme-variant-context'; + +export default { + title: 'Components/Timer', + parameters: {fullScreen: true}, +}; + +interface BaseArgs { + themeVariant: Variant; + minTimeUnit: TimeUnit; + maxTimeUnit: TimeUnit; + days: number; + hours: number; + minutes: number; + seconds: number; +} + +const SECOND = 1000; +const MINUTE = SECOND * 60; +const HOUR = MINUTE * 60; +const DAY = HOUR * 24; + +const baseArgs: BaseArgs = { + themeVariant: 'default', + minTimeUnit: 'seconds', + maxTimeUnit: 'hours', + days: 1, + hours: 0, + minutes: 0, + seconds: 0, +}; + +const baseArgTypes = { + themeVariant: { + options: ['default', 'inverse', 'alternative'], + control: {type: 'select'}, + }, + minTimeUnit: { + options: ['undefined', 'seconds', 'minutes', 'hours', 'days'], + control: {type: 'select'}, + }, + maxTimeUnit: { + options: ['undefined', 'seconds', 'minutes', 'hours', 'days'], + control: {type: 'select'}, + }, +}; + +type TextTimerArgs = BaseArgs & {labelType: 'none' | 'short' | 'long'}; + +export const TextTimerStory: StoryComponent = ({ + labelType, + themeVariant, + minTimeUnit, + maxTimeUnit, + days, + hours, + minutes, + seconds, +}) => { + const [remainingTime, setRemainingTime] = React.useState(); + const [endTimestamp, setEndTimestamp] = React.useState( + Date.now() + DAY * days + HOUR * hours + MINUTE * minutes + SECOND * seconds + ); + + React.useEffect(() => { + setEndTimestamp(Date.now() + DAY * days + HOUR * hours + MINUTE * minutes + SECOND * seconds); + }, [days, hours, minutes, seconds]); + + return ( + + + + + { + if (!isEqual(currentValue, remainingTime)) { + setRemainingTime(currentValue); + } + }} + /> + + + onProgress callback value + {remainingTime && ( + + {JSON.stringify(remainingTime, null, 2)} + + )} +
+ + + + + ); +}; + +TextTimerStory.storyName = 'TextTimer'; +TextTimerStory.args = { + labelType: 'none', + ...baseArgs, +}; +TextTimerStory.argTypes = { + ...baseArgTypes, + labelType: { + options: ['none', 'short', 'long'], + control: {type: 'select'}, + }, +}; + +type TimerArgs = BaseArgs & {boxed: boolean}; + +export const TimerStory: StoryComponent = ({ + themeVariant, + minTimeUnit, + maxTimeUnit, + days, + hours, + minutes, + seconds, + boxed, +}) => { + const [remainingTime, setRemainingTime] = React.useState(); + const [endTimestamp, setEndTimestamp] = React.useState( + Date.now() + DAY * days + HOUR * hours + MINUTE * minutes + SECOND * seconds + ); + + React.useEffect(() => { + setEndTimestamp(Date.now() + DAY * days + HOUR * hours + MINUTE * minutes + SECOND * seconds); + }, [days, hours, minutes, seconds]); + + return ( + + + + { + if (!isEqual(currentValue, remainingTime)) { + setRemainingTime(currentValue); + } + }} + /> + + + onProgress callback value + {remainingTime && ( + + {JSON.stringify(remainingTime, null, 2)} + + )} + + + + + ); +}; + +TimerStory.storyName = 'Timer'; +TimerStory.args = { + ...baseArgs, + boxed: false, +}; +TimerStory.argTypes = { + ...baseArgTypes, +}; diff --git a/src/__tests__/timer-test.tsx b/src/__tests__/timer-test.tsx new file mode 100644 index 0000000000..6654e977a1 --- /dev/null +++ b/src/__tests__/timer-test.tsx @@ -0,0 +1,167 @@ +import * as React from 'react'; +import {render, screen} from '@testing-library/react'; +import {TextTimer, ThemeContextProvider, Timer} from '..'; +import {makeTheme} from './test-utils'; +import {act} from 'react-dom/test-utils'; + +const SECOND = 1000; +const DAY = SECOND * 60 * 60 * 24; + +test('Timer', async () => { + jest.useFakeTimers(); + + render( + + + + ); + + await screen.findByRole('timer'); + + await screen.findByText('0 horas, 1 minuto y 0 segundos'); + + act(() => jest.advanceTimersByTime(SECOND)); + await screen.findByText('0 horas, 0 minutos y 59 segundos'); + + act(() => jest.advanceTimersByTime(SECOND * 29)); + await screen.findByText('0 horas, 0 minutos y 30 segundos'); + + act(() => jest.runAllTimers()); + await screen.findByText('0 horas, 0 minutos y 0 segundos'); +}); + +test('Timer - timestamp from the past', async () => { + jest.useFakeTimers(); + + render( + + + + ); + + await screen.findByText('0 horas, 0 minutos y 0 segundos'); + + act(() => jest.runAllTimers()); + await screen.findByText('0 horas, 0 minutos y 0 segundos'); +}); + +test('Timer - from minutes to days', async () => { + jest.useFakeTimers(); + + render( + + + + ); + + await screen.findByText('0 días, 0 horas y 1 minuto'); + + act(() => jest.advanceTimersByTime(SECOND)); + await screen.findByText('0 días, 0 horas y 1 minuto'); + + act(() => jest.advanceTimersByTime(SECOND)); + await screen.findByText('0 días, 0 horas y 0 minutos'); + + act(() => jest.runAllTimers()); + await screen.findByText('0 días, 0 horas y 0 minutos'); +}); + +test('Timer - renders only minimum unit if minTimeUnit is bigger than maxTimeUnit', async () => { + jest.useFakeTimers(); + + render( + + + + ); + + await screen.findByText('24 horas'); + + act(() => jest.runAllTimers()); + await screen.findByText('0 horas'); +}); + +test('Timer - render days', async () => { + jest.useFakeTimers(); + + render( + + + + ); + + await screen.findByText('1 día, 0 horas, 0 minutos y 0 segundos'); + + act(() => jest.advanceTimersByTime(SECOND)); + await screen.findByText('0 días, 23 horas, 59 minutos y 59 segundos'); + + act(() => jest.runAllTimers()); + await screen.findByText('0 días, 0 horas, 0 minutos y 0 segundos'); +}); + +test('Timer - render only seconds', async () => { + jest.useFakeTimers(); + + render( + + + + ); + + await screen.findByText('86400 segundos'); + + act(() => jest.runAllTimers()); + await screen.findByText('0 segundos'); +}); + +test('Timer - component is accessible', async () => { + jest.useFakeTimers(); + + render( + + + + ); + + await screen.findByLabelText('A label. 0 horas, 0 minutos y 1 segundo'); +}); + +test("TextTimer - doesn't render days", async () => { + jest.useFakeTimers(); + + render( + + + + ); + + await screen.findByText('24 horas, 0 minutos y 0 segundos'); + + act(() => jest.advanceTimersByTime(SECOND)); + await screen.findByText('23 horas, 59 minutos y 59 segundos'); + + act(() => jest.runAllTimers()); + await screen.findByText('0 horas, 0 minutos y 0 segundos'); +}); + +test('TextTimer - component is accessible', async () => { + jest.useFakeTimers(); + + render( + + + + ); + + await screen.findByLabelText('A label. 0 horas, 0 minutos y 1 segundo'); +}); diff --git a/src/index.tsx b/src/index.tsx index eef2a59a9b..758babd32e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -67,6 +67,7 @@ export {default as HighlightedCard} from './highlighted-card'; export {default as Stepper} from './stepper'; export {ProgressBar, ProgressBarStepped} from './progress-bar'; export {VerticalMosaic, HorizontalMosaic} from './mosaic'; +export {Timer, TextTimer} from './timer'; export { MediaCard, DataCard, diff --git a/src/theme.tsx b/src/theme.tsx index c9ff0725fd..6c3111b14a 100644 --- a/src/theme.tsx +++ b/src/theme.tsx @@ -53,6 +53,21 @@ const TEXTS_ES = { counterQuantity: 'cantidad', counterMinValue: 'mínimo', counterMaxValue: 'máximo', + timerDaysShortLabel: 'd', + timerHoursShortLabel: 'h', + timerMinutesShortLabel: 'min', + timerSecondsShortLabel: 's', + timerAnd: 'y', + timerDayLongLabel: 'día', + timerDaysLongLabel: 'días', + timerHourLongLabel: 'hora', + timerHoursLongLabel: 'horas', + timerMinuteLongLabel: 'minuto', + timerMinutesLongLabel: 'minutos', + timerSecondLongLabel: 'segundo', + timerSecondsLongLabel: 'segundos', + timerDisplayMinutesLabel: 'min', + timerDisplaySecondsLabel: 'seg', }; const TEXTS_EN: ThemeTexts = { @@ -101,6 +116,21 @@ const TEXTS_EN: ThemeTexts = { counterQuantity: 'quantity', counterMinValue: 'minimum of', counterMaxValue: 'maximum of', + timerDaysShortLabel: 'd', + timerHoursShortLabel: 'h', + timerMinutesShortLabel: 'min', + timerSecondsShortLabel: 's', + timerAnd: 'and', + timerDayLongLabel: 'day', + timerDaysLongLabel: 'days', + timerHourLongLabel: 'hour', + timerHoursLongLabel: 'hours', + timerMinuteLongLabel: 'minute', + timerMinutesLongLabel: 'minutes', + timerSecondLongLabel: 'second', + timerSecondsLongLabel: 'seconds', + timerDisplayMinutesLabel: 'min', + timerDisplaySecondsLabel: 'sec', }; const TEXTS_DE: ThemeTexts = { @@ -149,6 +179,21 @@ const TEXTS_DE: ThemeTexts = { counterQuantity: 'menge', counterMinValue: 'minimal', counterMaxValue: 'maximal', + timerDaysShortLabel: 'Tg.', + timerHoursShortLabel: 'Std.', + timerMinutesShortLabel: 'Min.', + timerSecondsShortLabel: 'Sek.', + timerAnd: 'und', + timerDayLongLabel: 'Tag', + timerDaysLongLabel: 'Tage', + timerHourLongLabel: 'Stunde', + timerHoursLongLabel: 'Stunden', + timerMinuteLongLabel: 'Minute', + timerMinutesLongLabel: 'Minuten', + timerSecondLongLabel: 'Sekunde', + timerSecondsLongLabel: 'Sekunden', + timerDisplayMinutesLabel: 'Min.', + timerDisplaySecondsLabel: 'Sek.', }; const TEXTS_PT: ThemeTexts = { @@ -197,6 +242,21 @@ const TEXTS_PT: ThemeTexts = { counterQuantity: 'quantidade', counterMinValue: 'mínimo', counterMaxValue: 'máximo', + timerDaysShortLabel: 'd', + timerHoursShortLabel: 'h', + timerMinutesShortLabel: 'min', + timerSecondsShortLabel: 's', + timerAnd: 'e', + timerDayLongLabel: 'dia', + timerDaysLongLabel: 'dias', + timerHourLongLabel: 'hora', + timerHoursLongLabel: 'horas', + timerMinuteLongLabel: 'minuto', + timerMinutesLongLabel: 'minutos', + timerSecondLongLabel: 'segundo', + timerSecondsLongLabel: 'segundos', + timerDisplayMinutesLabel: 'min', + timerDisplaySecondsLabel: 'seg', }; export const getTexts = (locale: Locale): typeof TEXTS_ES => { diff --git a/src/timer.css.ts b/src/timer.css.ts new file mode 100644 index 0000000000..c65396b062 --- /dev/null +++ b/src/timer.css.ts @@ -0,0 +1,75 @@ +import {style} from '@vanilla-extract/css'; +import {sprinkles} from './sprinkles.css'; +import * as mq from './media-queries.css'; +import {pxToRem} from './utils/css'; +import {vars} from './skins/skin-contract.css'; + +export const timerWrapper = sprinkles({display: 'inline-block'}); + +export const inlineText = style({ + textDecoration: 'inherit', +}); + +export const unitContainer = style([ + inlineText, + sprinkles({ + display: 'inline-flex', + justifyContent: 'center', + }), +]); + +export const shortLabelText = style([ + inlineText, + sprinkles({ + display: 'inline-block', + }), +]); + +export const timerDisplayValue = style([ + sprinkles({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }), +]); + +export const boxedTimerDisplayValue = style([ + { + minWidth: pxToRem(64), + '@media': { + [mq.tabletOrSmaller]: { + minWidth: pxToRem(56), + }, + }, + }, +]); + +const baseBoxedTimerValueContainer = style([ + sprinkles({ + paddingX: 4, + paddingY: 8, + borderRadius: vars.borderRadii.container, + }), + { + '@media': { + [mq.tabletOrSmaller]: { + paddingTop: 10, + paddingBottom: 10, + }, + }, + }, +]); + +export const boxedTimerValueContainer = style([ + baseBoxedTimerValueContainer, + sprinkles({ + background: vars.colors.brandLow, + }), +]); + +export const boxedTimerValueContainerInverse = style([ + baseBoxedTimerValueContainer, + sprinkles({ + background: vars.colors.backgroundContainer, + }), +]); diff --git a/src/timer.tsx b/src/timer.tsx new file mode 100644 index 0000000000..38910c425d --- /dev/null +++ b/src/timer.tsx @@ -0,0 +1,421 @@ +'use client'; +import classNames from 'classnames'; +import * as React from 'react'; +import Box from './box'; +import {useAriaId, useIsomorphicLayoutEffect, useTheme} from './hooks'; +import Inline from './inline'; +import ScreenReaderOnly from './screen-reader-only'; +import {Text2, Text6} from './text'; +import {ThemeVariant, useThemeVariant} from './theme-variant-context'; +import * as styles from './timer.css'; +import {getPrefixedDataAttributes} from './utils/dom'; +import {isEqual} from './utils/helpers'; +import {isRunningAcceptanceTest} from './utils/platform'; + +import type {DataAttributes} from './utils/types'; + +const DAY_IN_HOURS = 24; +const HOUR_IN_MINUTES = 60; +const MINUTE_IN_SECONDS = 60; +const SECOND_IN_MS = 1000; + +const MINUTE_IN_MS = MINUTE_IN_SECONDS * SECOND_IN_MS; +const HOUR_IN_MS = HOUR_IN_MINUTES * MINUTE_IN_MS; +const DAY_IN_MS = DAY_IN_HOURS * HOUR_IN_MS; + +export type TimeUnit = 'days' | 'hours' | 'minutes' | 'seconds'; +type Label = 'none' | 'short' | 'long'; + +export interface RemainingTime { + days?: number; + hours?: number; + minutes?: number; + seconds?: number; +} + +interface BaseProps { + endTimestamp: Date | number; + minTimeUnit: TimeUnit; + maxTimeUnit: TimeUnit; + dataAttributes?: DataAttributes; + onProgress?: (value: RemainingTime) => void; + 'aria-label'?: string; +} + +interface TextTimerProps extends BaseProps { + labelType?: Label; +} + +interface TimerProps extends BaseProps { + boxed?: boolean; +} + +const shouldRenderUnit = ( + unit: TimeUnit, + minTimeUnit: TimeUnit, + maxTimeUnit: TimeUnit, + labelType?: Label +) => { + // If label is "none", days shouldn't be displayed + minTimeUnit = labelType === 'none' && minTimeUnit === 'days' ? 'hours' : minTimeUnit; + maxTimeUnit = labelType === 'none' && maxTimeUnit === 'days' ? 'hours' : maxTimeUnit; + + const unitsOrder: Array = ['seconds', 'minutes', 'hours', 'days']; + + const minValue = unitsOrder.indexOf(minTimeUnit); + // If max < min, we display only the min unit + const maxValue = Math.max(unitsOrder.indexOf(maxTimeUnit), minValue); + const unitValue = unitsOrder.indexOf(unit); + + return minValue <= unitValue && unitValue <= maxValue; +}; + +const getFilteredTimerValue = ( + timestamp: RemainingTime, + minTimeUnit: TimeUnit, + maxTimeUnit: TimeUnit, + labelType?: Label +) => { + return ( + [ + {unit: 'days', value: timestamp.days}, + {unit: 'hours', value: timestamp.hours}, + {unit: 'minutes', value: timestamp.minutes}, + {unit: 'seconds', value: timestamp.seconds}, + ] as Array<{unit: TimeUnit; value: number}> + ).filter((item) => shouldRenderUnit(item.unit, minTimeUnit, maxTimeUnit, labelType)); +}; + +const getRemainingTime = (endTimestamp: Date | number) => { + // Always return 0 ms remaining for screenshot tests to avoid unstable values caused by delays in browser + if (isRunningAcceptanceTest()) { + return 0; + } + + return Math.max( + 0, + (typeof endTimestamp === 'object' ? endTimestamp : new Date(endTimestamp)).valueOf() - Date.now() + ); +}; + +const useTimerState = ({ + endTimestamp, + labelType, + minTimeUnit, + maxTimeUnit, + onProgress, +}: { + endTimestamp: Date | number; + labelType?: Label; + minTimeUnit: TimeUnit; + maxTimeUnit: TimeUnit; + onProgress?: (value: RemainingTime) => void; +}) => { + const [remainingTime, setRemainingTime] = React.useState(getRemainingTime(endTimestamp)); + + useIsomorphicLayoutEffect(() => { + let intervalId: NodeJS.Timeout; + + const updateCurrentTime = () => { + const currentRemainingTime = getRemainingTime(endTimestamp); + setRemainingTime(currentRemainingTime); + + // Stop computing values if there is no time remaining + if (!currentRemainingTime) { + clearInterval(intervalId); + } + }; + + if (!isRunningAcceptanceTest()) { + updateCurrentTime(); + intervalId = setInterval(updateCurrentTime, SECOND_IN_MS); + return () => clearInterval(intervalId); + } + }, [endTimestamp]); + + const shouldRenderDays = shouldRenderUnit('days', minTimeUnit, maxTimeUnit, labelType); + const shouldRenderHours = shouldRenderUnit('hours', minTimeUnit, maxTimeUnit, labelType); + const shouldRenderMinutes = shouldRenderUnit('minutes', minTimeUnit, maxTimeUnit, labelType); + + const maximumRenderedUnit = shouldRenderDays + ? 'days' + : shouldRenderHours + ? 'hours' + : shouldRenderMinutes + ? 'minutes' + : 'seconds'; + + const currentHours = Math.floor(remainingTime / HOUR_IN_MS) % 24; + const currentMinutes = Math.floor(remainingTime / MINUTE_IN_MS) % 60; + const currentSeconds = Math.floor(remainingTime / SECOND_IN_MS) % 60; + + const days = Math.floor(remainingTime / DAY_IN_MS); + + // if hours is the maximum unit, add remaining days + const hours = maximumRenderedUnit === 'hours' ? currentHours + days * DAY_IN_HOURS : currentHours; + + // if minutes is the maximum unit, add remaining days and hours + const minutes = + maximumRenderedUnit === 'minutes' + ? currentMinutes + HOUR_IN_MINUTES * (days * DAY_IN_HOURS + hours) + : currentMinutes; + + // if minutes is the maximum unit, add remaining days, hours and minutes + const seconds = + maximumRenderedUnit === 'seconds' + ? currentSeconds + + MINUTE_IN_SECONDS * (days * DAY_IN_HOURS * HOUR_IN_MINUTES + hours * HOUR_IN_MINUTES + minutes) + : currentSeconds; + + const [timerValue, setTimerValue] = React.useState( + getFilteredTimerValue({days, hours, minutes, seconds}, minTimeUnit, maxTimeUnit, labelType) + ); + + React.useEffect(() => { + const currentTimerValue = getFilteredTimerValue( + {days, hours, minutes, seconds}, + minTimeUnit, + maxTimeUnit, + labelType + ); + + if (!isEqual(currentTimerValue, timerValue)) { + setTimerValue(currentTimerValue); + const timestampValue: RemainingTime = {}; + currentTimerValue.forEach((item) => (timestampValue[item.unit] = item.value)); + onProgress?.(timestampValue); + } + }, [days, hours, minutes, seconds, labelType, minTimeUnit, maxTimeUnit, timerValue, onProgress]); + + return timerValue; +}; + +export const TextTimer: React.FC = ({ + endTimestamp, + labelType = 'none', + minTimeUnit, + maxTimeUnit, + onProgress, + dataAttributes, + 'aria-label': ariaLabel, +}) => { + const {texts} = useTheme(); + const labelId = useAriaId(); + + const timerValue = useTimerState({endTimestamp, labelType, minTimeUnit, maxTimeUnit, onProgress}); + + const unitShortLabel: {[key in TimeUnit]: string} = { + days: texts.timerDaysShortLabel, + hours: texts.timerHoursShortLabel, + minutes: texts.timerMinutesShortLabel, + seconds: texts.timerSecondsShortLabel, + }; + + const unitLabel: {[key in TimeUnit]: string} = { + days: texts.timerDayLongLabel, + hours: texts.timerHourLongLabel, + minutes: texts.timerMinuteLongLabel, + seconds: texts.timerSecondLongLabel, + }; + + const unitLabelPlural: {[key in TimeUnit]: string} = { + days: texts.timerDaysLongLabel, + hours: texts.timerHoursLongLabel, + minutes: texts.timerMinutesLongLabel, + seconds: texts.timerSecondsLongLabel, + }; + + const renderFormattedNumber = (value: number) => { + const digitCount = Math.max(String(value).length, labelType === 'long' ? 1 : 2); + + // Set container's minWidth in ch to avoid it from updating it's width when numbers change + return ( + + {String(value).padStart(digitCount, '0')} + + ); + }; + + const renderTime = () => { + switch (labelType) { + case 'none': + return timerValue.map((item, index) => ( + + {index > 0 && ':'} + {renderFormattedNumber(item.value)} + + )); + + case 'short': + return timerValue.map((item, index) => ( + + {index > 0 && ' '} + + {renderFormattedNumber(item.value)} + {` ${unitShortLabel[item.unit]}`} + + + )); + + case 'long': + default: + return timerValue.map((item, index) => ( + + {index > 0 && ' '} + {renderFormattedNumber(item.value)} + {` ${item.value === 1 ? unitLabel[item.unit] : unitLabelPlural[item.unit]}`} + {index === timerValue.length - 2 && ` ${texts.timerAnd}`} + {index < timerValue.length - 2 && ','} + + )); + } + }; + + const timerLabel = timerValue + .map( + (item, index) => + `${item.value} ${item.value === 1 ? unitLabel[item.unit] : unitLabelPlural[item.unit]}${ + index === timerValue.length - 1 + ? '' + : index === timerValue.length - 2 + ? ` ${texts.timerAnd} ` + : ', ' + }` + ) + .join(''); + + return ( + + + {ariaLabel ? `${ariaLabel}. ${timerLabel}` : timerLabel} + + + + {renderTime()} + + + ); +}; + +export const Timer: React.FC = ({ + boxed, + endTimestamp, + minTimeUnit, + maxTimeUnit, + onProgress, + dataAttributes, + 'aria-label': ariaLabel, +}) => { + const {texts} = useTheme(); + const labelId = useAriaId(); + const themeVariant = useThemeVariant(); + + const timerValue = useTimerState({endTimestamp, minTimeUnit, maxTimeUnit, onProgress}); + + const displayLabel: {[key in TimeUnit]: string} = { + days: texts.timerDayLongLabel, + hours: texts.timerHourLongLabel, + minutes: texts.timerDisplayMinutesLabel, + seconds: texts.timerDisplaySecondsLabel, + }; + + const displayLabelPlural: {[key in TimeUnit]: string} = { + days: texts.timerDaysLongLabel, + hours: texts.timerHoursLongLabel, + minutes: texts.timerDisplayMinutesLabel, + seconds: texts.timerDisplaySecondsLabel, + }; + + const unitLabel: {[key in TimeUnit]: string} = { + days: texts.timerDayLongLabel, + hours: texts.timerHourLongLabel, + minutes: texts.timerMinuteLongLabel, + seconds: texts.timerSecondLongLabel, + }; + + const unitLabelPlural: {[key in TimeUnit]: string} = { + days: texts.timerDaysLongLabel, + hours: texts.timerHoursLongLabel, + minutes: texts.timerMinutesLongLabel, + seconds: texts.timerSecondsLongLabel, + }; + + const renderFormattedNumber = (value: number) => { + const digitCount = Math.max(String(value).length, 2); + + // Set container's minWidth in ch to avoid it from updating it's width when numbers change + return ( + +
+ {String(value).padStart(digitCount, '0')} +
+
+ ); + }; + + const timerLabel = timerValue + .map( + (item, index) => + `${item.value} ${item.value === 1 ? unitLabel[item.unit] : unitLabelPlural[item.unit]}${ + index === timerValue.length - 1 + ? '' + : index === timerValue.length - 2 + ? ` ${texts.timerAnd} ` + : ', ' + }` + ) + .join(''); + + const renderTime = () => { + return timerValue.map((item, index) => ( + + +
+ {renderFormattedNumber(item.value)} + + {item.value === 1 ? displayLabel[item.unit] : displayLabelPlural[item.unit]} + +
+
+
+ )); + }; + + return ( +
+ + {ariaLabel ? `${ariaLabel}. ${timerLabel}` : timerLabel} + + +
+ + {renderTime()} + +
+
+ ); +}; diff --git a/src/utils/__tests__/helpers-test.tsx b/src/utils/__tests__/helpers-test.tsx index bacd935737..272f714fa1 100644 --- a/src/utils/__tests__/helpers-test.tsx +++ b/src/utils/__tests__/helpers-test.tsx @@ -131,6 +131,7 @@ test('isEqual happy case', () => { n: 123, s: 'abc', b: true, + nan: NaN, nul: null, und: undefined, arr: [1, false, null, undefined, new Date(1234567890), {a: 1, b: 2, c: 3}], @@ -143,6 +144,7 @@ test('isEqual happy case', () => { n: 123, s: 'abc', b: true, + nan: NaN, nul: null, und: undefined, arr: [1, false, null, undefined, new Date(1234567890), {a: 1, b: 2, c: 3}], diff --git a/src/utils/helpers.tsx b/src/utils/helpers.tsx index d9b1bb33c9..a2a97f9cbe 100644 --- a/src/utils/helpers.tsx +++ b/src/utils/helpers.tsx @@ -141,6 +141,10 @@ export const isEqual = (a: unknown, b: unknown): boolean => { return true; } + if (typeof a === 'number' && typeof b === 'number' && isNaN(a) && isNaN(b)) { + return true; + } + if (isPrimitive(a) || isPrimitive(b)) { return false; }