diff --git a/system/libs/figa-hooks/src/lib/use-element-size/defs.ts b/system/libs/figa-hooks/src/lib/use-element-size/defs.ts index 8d543a3bc..f6394dc17 100644 --- a/system/libs/figa-hooks/src/lib/use-element-size/defs.ts +++ b/system/libs/figa-hooks/src/lib/use-element-size/defs.ts @@ -1,3 +1,5 @@ +import type { MutableRefObject } from 'react'; + interface ElementSize { width: number; height: number; @@ -19,12 +21,14 @@ type ElementSizeState = UndetectedState | DetectedState | UnsupportedState; type ElementSizeStateStatus = ElementSizeState['status']; -/** Configuration object. */ -interface UseElementSizeConfig { - /** It quantifies how much time is needed to broadcast the next event in milliseconds. */ +interface ElementSizeConfig { delay?: number; } +type ElementSizeReturn = Readonly< + [ElementSizeState, MutableRefObject] +>; + export type { UndetectedState, ElementSize, @@ -32,5 +36,6 @@ export type { UnsupportedState, ElementSizeStateStatus, ElementSizeState, - UseElementSizeConfig, + ElementSizeReturn, + ElementSizeConfig, }; diff --git a/system/libs/figa-hooks/src/lib/use-element-size/use-element-size.test.tsx b/system/libs/figa-hooks/src/lib/use-element-size/use-element-size.test.tsx index 6a6f725d6..fea49c18d 100644 --- a/system/libs/figa-hooks/src/lib/use-element-size/use-element-size.test.tsx +++ b/system/libs/figa-hooks/src/lib/use-element-size/use-element-size.test.tsx @@ -52,12 +52,12 @@ describe('Element size can be detected when: ', () => { expect(observeSpy).toHaveBeenCalledTimes(1); expect(observeSpy).toHaveBeenCalledWith(document.body); - expect(result.current.state).toEqual({ + expect(result.current[0]).toEqual({ status: 'undetected', } as ElementSizeState); await waitFor(() => { - expect(result.current.state).toEqual({ + expect(result.current[0]).toEqual({ status: 'detected', height: HEIGHT, width: WIDTH, @@ -67,7 +67,7 @@ describe('Element size can be detected when: ', () => { it('updates state if listening native HTML element', async () => { const ComponentFixture = () => { - const { ref, state } = useElementSize(); + const [state, ref] = useElementSize(); return (
{state.status === 'detected' diff --git a/system/libs/figa-hooks/src/lib/use-element-size/use-element-size.ts b/system/libs/figa-hooks/src/lib/use-element-size/use-element-size.ts index adf740078..727db2163 100644 --- a/system/libs/figa-hooks/src/lib/use-element-size/use-element-size.ts +++ b/system/libs/figa-hooks/src/lib/use-element-size/use-element-size.ts @@ -1,17 +1,22 @@ import { useEffect, useRef, useState, useMemo } from 'react'; import { Subject, throttleTime } from 'rxjs'; -import type { ElementSizeState, UseElementSizeConfig } from './defs'; +import type { + ElementSizeState, + ElementSizeConfig, + ElementSizeReturn, +} from './defs'; +import { useIsomorphicLayoutEffect } from '../use-isomorphic-layout-effect'; /** * The hook responsible for detecting the height and width of * any HTML element. By default it checks body. * * It returns reference and state to work with. - * @param {UseElementSizeConfig} config - Configuration object. + * @param {ElementSizeConfig} config - Configuration object. */ const useElementSize = ( - config?: UseElementSizeConfig -) => { + config?: ElementSizeConfig +): ElementSizeReturn => { const [state, setState] = useState({ status: 'undetected', }); @@ -19,9 +24,11 @@ const useElementSize = ( const ref = useRef(null); const observerRef = useRef(null); - const changed = useMemo(() => new Subject(), []); - // eslint-disable-next-line react-hooks/exhaustive-deps - const changed$ = useMemo(() => changed.asObservable(), []); + const { changed, changed$ } = useMemo(() => { + const changed = new Subject(); + const changed$ = changed.asObservable(); + return { changed, changed$ }; + }, []); useEffect(() => { const sub = changed$.pipe(throttleTime(config?.delay ?? 150)).subscribe({ @@ -33,10 +40,9 @@ const useElementSize = ( return () => { sub.unsubscribe(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [changed$, config?.delay]); - useEffect(() => { + useIsomorphicLayoutEffect(() => { const observeElement = () => { if (!ref?.current && !document.body) { changed.next({ status: 'unsupported' }); @@ -61,13 +67,9 @@ const useElementSize = ( return () => { observerRef.current?.disconnect(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [changed]); - return { - state, - ref, - }; + return [state, ref]; }; export { useElementSize }; diff --git a/system/libs/figa-ui/src/lib/creator-layout/creator-layout.tsx b/system/libs/figa-ui/src/lib/creator-layout/creator-layout.tsx index 6abbc9047..1d00a799c 100644 --- a/system/libs/figa-ui/src/lib/creator-layout/creator-layout.tsx +++ b/system/libs/figa-ui/src/lib/creator-layout/creator-layout.tsx @@ -124,7 +124,7 @@ const CreatorLayout = ({ ...props }: CreatorLayoutProps) => { const [view, setView] = useState('undetected'); - const { state: size } = useElementSize({ delay: 20 }); + const [size] = useElementSize({ delay: 20 }); const [Code, Preview] = children; diff --git a/system/libs/figa-ui/src/lib/timeline/check.svg b/system/libs/figa-ui/src/lib/timeline/check.svg new file mode 100644 index 000000000..069b6c944 --- /dev/null +++ b/system/libs/figa-ui/src/lib/timeline/check.svg @@ -0,0 +1,15 @@ + + + + + + + + \ No newline at end of file diff --git a/system/libs/figa-ui/src/lib/timeline/defs.ts b/system/libs/figa-ui/src/lib/timeline/defs.ts new file mode 100644 index 000000000..d462f7a75 --- /dev/null +++ b/system/libs/figa-ui/src/lib/timeline/defs.ts @@ -0,0 +1,8 @@ +interface IEvent { + date: string; + title: string; + description: string; + isReached: boolean; +} + +export type { IEvent }; diff --git a/system/libs/figa-ui/src/lib/timeline/index.ts b/system/libs/figa-ui/src/lib/timeline/index.ts new file mode 100644 index 000000000..a2189ffab --- /dev/null +++ b/system/libs/figa-ui/src/lib/timeline/index.ts @@ -0,0 +1,2 @@ +export * from './defs'; +export * from './timeline'; diff --git a/system/libs/figa-ui/src/lib/timeline/mocks.ts b/system/libs/figa-ui/src/lib/timeline/mocks.ts new file mode 100644 index 000000000..367d6d53a --- /dev/null +++ b/system/libs/figa-ui/src/lib/timeline/mocks.ts @@ -0,0 +1,32 @@ +export const mocks = [ + { + date: 'Jan 20', + title: 'Start of Evaluation', + description: 'Preparation', + }, + { + date: 'Feb 14', + title: 'Initial Scoping', + description: 'Data checking', + }, + { + date: 'March 8', + title: 'Validation', + description: 'Completed proof of concept', + }, + { + date: 'Apr 30', + title: 'Contracting', + description: 'Signed contract', + }, + { + date: 'June 7', + title: 'Migration', + description: 'All information is migrated', + }, + { + date: 'Sep 16', + title: 'Global Launch', + description: 'In Asia, Australia, Latin America', + }, +]; diff --git a/system/libs/figa-ui/src/lib/timeline/timeline.stories.tsx b/system/libs/figa-ui/src/lib/timeline/timeline.stories.tsx new file mode 100644 index 000000000..838d90b88 --- /dev/null +++ b/system/libs/figa-ui/src/lib/timeline/timeline.stories.tsx @@ -0,0 +1,22 @@ +import type { Story, Meta } from '@storybook/react'; + +import { Timeline } from './timeline'; +import { Box } from '../box'; + +export default { + component: Timeline, + title: 'Timeline', +} as Meta; + +const Template: Story = () => { + return ( + + + + + + ); +}; + +export const Default = Template.bind({}); +Default.args = {}; diff --git a/system/libs/figa-ui/src/lib/timeline/timeline.tsx b/system/libs/figa-ui/src/lib/timeline/timeline.tsx new file mode 100644 index 000000000..f75b4f423 --- /dev/null +++ b/system/libs/figa-ui/src/lib/timeline/timeline.tsx @@ -0,0 +1,175 @@ +import { useEffect, useRef, useState } from 'react'; +import styled from 'styled-components'; + +import { Font } from '../font'; +import { mocks } from './mocks'; + +const StyledTimeline = styled.div` + width: 900px; + height: 20px; + background-color: #ffffff33; + border-radius: 50px; + display: flex; + position: relative; +`; + +const EventContainer = styled.div` + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: space-between; + & > div { + background-color: black; + width: 50px; + height: 50px; + border-radius: 50%; + border: 3px solid; + position: relative; + z-index: 1; + } +`; + +const ProgressBar = styled.div` + position: absolute; + height: 20px; + width: ${({ width }: { width: string }) => `${width}%`}; + left: 0; + top: 0; + background-color: #35b78b; + border-radius: 50px; +`; + +const MoveProgressBar = styled('input')` + position: absolute; + width: 500px; + transform: rotate(-90deg); + transform-origin: center; + left: -200px; +`; + +const Timeline = () => { + const [progressBar, setProgressBar] = useState('30'); + const [reachedEventIndex, setReachedEventIndex] = useState(0); + + const eventRefs = useRef([]); + const progressBarRef = useRef(null); + + useEffect(() => { + const positions = eventRefs.current.map((ref) => { + return ref.getBoundingClientRect().x; + }); + + const checkIfEventIsReached = () => { + const ref = progressBarRef.current; + if (ref === null) throw Error('ProgressBar ref is null'); + const progressBarPosition = ref.getBoundingClientRect().right; + const reachedEventsPositions = positions.filter( + (pos) => pos <= progressBarPosition + ); + + setReachedEventIndex(reachedEventsPositions.length - 1); + }; + checkIfEventIsReached(); + }, [progressBar]); + + return ( + <> + + + {mocks.map((event, index) => ( +
{ + if (ref === null) return; + eventRefs.current[index] = ref; + }} + style={{ + position: 'relative', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + borderColor: + index === 0 || index <= reachedEventIndex + ? '#35b78b' + : '#ffffff33', + transition: '0.2s', + }} + > + {index <= reachedEventIndex && index !== mocks.length - 1 && ( + + + + + + )} + {index === mocks.length - 1 && ( + + {`${progressBar}`} + % + + )} +
+ + {event.title} + + + {event.description} + +
+
+ ))} +
+ +
+ setProgressBar(e.target.value)} + /> + + ); +}; + +export { Timeline };