Skip to content

Commit

Permalink
39. Add ProgressCircle component
Browse files Browse the repository at this point in the history
  • Loading branch information
Adrian Połubiński authored and polubis committed May 21, 2023
1 parent cfc050c commit 41706e0
Show file tree
Hide file tree
Showing 12 changed files with 264 additions and 7 deletions.
8 changes: 1 addition & 7 deletions system/apps/jamjam/pages/improvisation-assistant.tsx
Expand Up @@ -32,13 +32,7 @@ const ImprovisationAssistant = () => {
]}
action={<div>Some action</div>}
/>
<div
style={{
padding: '24px',
}}
>
<GuitarFretboard notation="bmoll" guitar={guitar} />
</div>
<GuitarFretboard notation="bmoll" guitar={guitar} />
</>
);
};
Expand Down
1 change: 1 addition & 0 deletions system/libs/figa-ui/src/index.ts
Expand Up @@ -8,4 +8,5 @@ export * from './lib/navigation';
export * from './lib/logo';
export * from './lib/link';
export * from './lib/emoji-picker';
export * from './lib/progress-circle';
export * from './lib/shared';
1 change: 1 addition & 0 deletions system/libs/figa-ui/src/lib/emoji-picker/emoji-picker.tsx
Expand Up @@ -13,6 +13,7 @@ const EmojiPicker = ({
onSelect,
}: EmojiPickerProps) => {
const handleSelect: MouseEventHandler<HTMLButtonElement> = (e) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
onSelect(e.currentTarget.getAttribute('data-emoji')!);
};

Expand Down
@@ -0,0 +1,39 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Progress works when [FRAGILE] can change timer size 1`] = `
<DocumentFragment>
<div
class="progress-circle"
style="width: 120px; height: 120px;"
>
<div
class="progress-circle-front"
style="transform: scale(1);"
/>
<h6
class="font font-h6 progress-circle-text"
>
5.0s
</h6>
</div>
</DocumentFragment>
`;

exports[`Progress works when [FRAGILE] renders with default properties setup 1`] = `
<DocumentFragment>
<div
class="progress-circle"
style="width: 100px; height: 100px;"
>
<div
class="progress-circle-front"
style="transform: scale(1);"
/>
<h6
class="font font-h6 progress-circle-text"
>
5.0s
</h6>
</div>
</DocumentFragment>
`;
17 changes: 17 additions & 0 deletions system/libs/figa-ui/src/lib/progress-circle/defs.ts
@@ -0,0 +1,17 @@
import type { ReactNode } from 'react';

type ProgressCircleMS = number;

type ProgressCircleChildren = (ms: ProgressCircleMS) => ReactNode;

interface ProgressCircleProps {
className?: string;
ms?: ProgressCircleMS;
interval?: number;
jump?: number;
size?: number;
onEnd?: () => void;
children?: ProgressCircleChildren;
}

export type { ProgressCircleProps, ProgressCircleMS, ProgressCircleChildren };
1 change: 1 addition & 0 deletions system/libs/figa-ui/src/lib/progress-circle/index.ts
@@ -0,0 +1 @@
export * from './progress-circle';
@@ -0,0 +1,22 @@
import type { Story, Meta } from '@storybook/react';

import { ProgressCircle } from './progress-circle';

export default {
component: ProgressCircle,
title: 'ProgressCircle',
} as Meta;

const Template: Story = () => {
return (
<div style={{ padding: '24px' }}>
<ProgressCircle />
<ProgressCircle size={150} jump={1000} />
<ProgressCircle size={50} interval={100} />
<ProgressCircle size={300} jump={1000} ms={3000} />
</div>
);
};

export const Default = Template.bind({});
Default.args = {};
@@ -0,0 +1,74 @@
import { render, screen, waitFor } from '@testing-library/react';

import { ProgressCircle } from './progress-circle';

describe('Progress works when', () => {
it('[FRAGILE] renders with default properties setup', () => {
const { asFragment } = render(<ProgressCircle />);

expect(asFragment()).toMatchSnapshot();
});

it('uses default text', () => {
render(<ProgressCircle />);

screen.getByText('5.0s');
});

it('allows to pass custom children text', () => {
render(<ProgressCircle children={(ms) => ms + 'seconds'} />);

screen.getByText('5000seconds');
});

it('[FRAGILE] assigns classes', () => {
const { container } = render(<ProgressCircle />);

expect(container.querySelectorAll('.progress-circle').length).toBe(1);
expect(container.querySelectorAll('.progress-circle-front').length).toBe(1);
expect(container.querySelectorAll('.progress-circle-text').length).toBe(1);
});

it('parent component can react when timer ends', async () => {
const onEndSpy = jest.fn();

render(<ProgressCircle onEnd={onEndSpy} ms={500} />);

await waitFor(() => {
expect(onEndSpy).toHaveBeenCalledTimes(1);
});
});

it('interval is cleaned when unmounts', () => {
const clearIntervalSpy = jest.spyOn(global, 'clearInterval');

const { unmount } = render(<ProgressCircle ms={1000} />);

unmount();

expect(clearIntervalSpy).toHaveBeenCalledTimes(1);
expect(clearIntervalSpy).toHaveBeenCalledWith(expect.any(Number));
});

it('there is a option to change timer starting time', async () => {
render(<ProgressCircle ms={500} />);

await waitFor(() => {
screen.getByText('0.0s');
});
});

it('[FRAGILE] can change timer size', () => {
const { asFragment } = render(<ProgressCircle size={120} />);

expect(asFragment()).toMatchSnapshot();
});

it('allows to assign custom class', () => {
const { container } = render(<ProgressCircle className="my-class" />);

expect(container.querySelectorAll('.progress-circle.my-class').length).toBe(
1
);
});
});
73 changes: 73 additions & 0 deletions system/libs/figa-ui/src/lib/progress-circle/progress-circle.tsx
@@ -0,0 +1,73 @@
import type { ProgressCircleProps, ProgressCircleChildren } from './defs';
import { useState, useEffect, useRef } from 'react';

import c from 'classnames';
import { Font } from '../font';

const toPercentage = (value: number, decimals = 1): number =>
parseInt((value * 100).toFixed(decimals));

const defaultChildren: ProgressCircleChildren = (ms) =>
(ms / 1000).toFixed(1) + 's';

const ProgressCircle = ({
className,
interval = 100,
jump = 100,
ms = 5000,
size = 100,
onEnd,
children = defaultChildren,
}: ProgressCircleProps) => {
const time = useRef(ms);
const [, setCounter] = useState(ms);
const intervalRef = useRef<NodeJS.Timer | null>(null);

const handleEnd = () => {
onEnd && onEnd();
};

const clearIntervalRef = () => {
intervalRef.current && clearInterval(intervalRef.current);
};

useEffect(() => {
intervalRef.current = setInterval(() => {
time.current -= jump;

setCounter(time.current);

if (time.current <= 0) {
clearIntervalRef();
handleEnd();
}
}, interval);

return () => {
clearIntervalRef();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<div
className={c('progress-circle', className)}
style={{
width: size,
height: size,
}}
>
<div
className="progress-circle-front"
style={{
transform: `scale(${toPercentage(time.current / ms) / 100})`,
}}
/>
<Font className="progress-circle-text" variant="h6">
{children(time.current)}
</Font>
</div>
);
};

export { ProgressCircle };
3 changes: 3 additions & 0 deletions system/libs/figa-ui/src/lib/theme-provider/defs.ts
Expand Up @@ -152,6 +152,9 @@ interface Theme {
};
};
};
progressCircle: {
bg: string;
};
}

type ThemeKey = 'dark' | 'light';
Expand Down
26 changes: 26 additions & 0 deletions system/libs/figa-ui/src/lib/theme-provider/global-style.ts
Expand Up @@ -412,6 +412,32 @@ const GlobalStyle = createGlobalStyle`
}
/* emoji-picker.tsx */
/* progress-circle.tsx */
.progress-circle {
position: relative;
.progress-circle-back,
.progress-circle-front {
${streched('absolute')}
border-radius: ${tokens.radius[1000]};
}
.progress-circle-front {
transition: 0.1s transform linear;
background: ${(props) => props.theme.progressCircle.bg};
}
.progress-circle-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
/* progress-circle.tsx */
`;

export { GlobalStyle };
6 changes: 6 additions & 0 deletions system/libs/figa-ui/src/lib/theme-provider/themes.ts
Expand Up @@ -154,6 +154,9 @@ const light: Theme = {
},
},
},
progressCircle: {
bg: tokens.gray[150],
},
};

const dark: Theme = {
Expand Down Expand Up @@ -220,6 +223,9 @@ const dark: Theme = {
},
},
},
progressCircle: {
bg: tokens.dark[50],
},
};

const themes: Themes = {
Expand Down

0 comments on commit 41706e0

Please sign in to comment.