Add AnimatedCheckmark
This commit is contained in:
parent
0162857021
commit
74f2b4e47e
3 changed files with 234 additions and 0 deletions
|
@ -0,0 +1,40 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from "react";
|
||||||
|
import { render, waitFor } from "@testing-library/react";
|
||||||
|
import '@testing-library/jest-dom/extend-expect';
|
||||||
|
import AnimatedCheckmark from "./AnimatedCheckmark";
|
||||||
|
import { IntlProvider } from "../../intl";
|
||||||
|
import I18n from '../../../i18n';
|
||||||
|
|
||||||
|
function Wrapper(props:React.PropsWithChildren<unknown>) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const translations = I18n.translations['en'] as any;
|
||||||
|
return (<IntlProvider messages={translations} locale={'en'}>
|
||||||
|
{props.children}
|
||||||
|
</IntlProvider>);
|
||||||
|
}
|
||||||
|
describe('Animated Checkmark', () => {
|
||||||
|
it('check if it renders', async () => {
|
||||||
|
expect.hasAssertions();
|
||||||
|
const {getByTestId} = render(<Wrapper><AnimatedCheckmark ariaLabel={"login.success"} role={"status"}></AnimatedCheckmark></Wrapper>);
|
||||||
|
const checkmark = getByTestId("CheckmarkTest");
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(checkmark).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('check Aria Label Message', async () => {
|
||||||
|
expect.hasAssertions();
|
||||||
|
const {queryByLabelText } = render(<Wrapper><AnimatedCheckmark ariaLabel={"You have successfully signed in."} role={"status"}></AnimatedCheckmark></Wrapper>);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(queryByLabelText("You have successfully signed in.")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('role has status as value', async () => {
|
||||||
|
expect.hasAssertions();
|
||||||
|
const {getByTestId } = render(<Wrapper><AnimatedCheckmark ariaLabel={"login.success"} role={"status"}></AnimatedCheckmark></Wrapper>);
|
||||||
|
const role = getByTestId("CheckmarkTest");
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(role).toHaveAttribute('role', 'status');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,25 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import AnimatedCheckmark, {AnimatedCheckmarkProps} from './AnimatedCheckmark';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'users/AnimatedCheckmark',
|
||||||
|
component: AnimatedCheckmark,
|
||||||
|
};
|
||||||
|
|
||||||
|
type TemplateArgs = AnimatedCheckmarkProps;
|
||||||
|
|
||||||
|
const CheckmarkTemplate = (args:TemplateArgs) => {
|
||||||
|
|
||||||
|
|
||||||
|
return <AnimatedCheckmark
|
||||||
|
{...args}
|
||||||
|
key={Math.random() /* so it reloads everytime props change */}></AnimatedCheckmark>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Checkmark = CheckmarkTemplate.bind({});
|
||||||
|
Checkmark.args = {
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
169
app/javascript/components/common/progress/AnimatedCheckmark.tsx
Normal file
169
app/javascript/components/common/progress/AnimatedCheckmark.tsx
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
// from: https://github.com/davidwilson3/react-typescript-checkmark/blob/1de3e0362965602d4345868f1f876aa54a96d5b6/src/checkmark.tsx
|
||||||
|
import React from 'react';
|
||||||
|
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
interface StyledProps {
|
||||||
|
animationDuration: number;
|
||||||
|
backgroundColor: string;
|
||||||
|
checkColor: string;
|
||||||
|
checkThickness: number;
|
||||||
|
explosion: number;
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = (makeStyles((theme:Theme) =>
|
||||||
|
createStyles({
|
||||||
|
root: {
|
||||||
|
display: "block",
|
||||||
|
marginLeft: "auto",
|
||||||
|
marginRight: "auto",
|
||||||
|
borderRadius: "50%",
|
||||||
|
width: (props:StyledProps) => props.width,
|
||||||
|
height: (props:StyledProps) => props.height,
|
||||||
|
stroke: (props:StyledProps) => props.checkColor,
|
||||||
|
strokeWidth: (props:StyledProps) => props.checkThickness,
|
||||||
|
strokeMiterlimit: 10,
|
||||||
|
animation: (props:StyledProps) => `$fill ${props.animationDuration * 0.66}s ease-in-out 0.4s
|
||||||
|
forwards,
|
||||||
|
$scale 0.3s ease-in-out 0.9s both`,
|
||||||
|
},
|
||||||
|
circle: {
|
||||||
|
strokeDasharray: 166,
|
||||||
|
strokeDashoffset: 166,
|
||||||
|
strokeWidth: (props:StyledProps) => props.checkThickness,
|
||||||
|
strokeMiterlimit: 10,
|
||||||
|
stroke: (props:StyledProps) => props.backgroundColor || theme.palette.success.main,
|
||||||
|
fill: "none",
|
||||||
|
animation: (props:StyledProps) => `$stroke-keyframe ${props.animationDuration}s
|
||||||
|
cubic-bezier(0.65, 0, 0.45, 1) forwards`,
|
||||||
|
},
|
||||||
|
checkmark: {
|
||||||
|
transformOrigin: "50% 50%",
|
||||||
|
strokeDasharray: 48,
|
||||||
|
strokeDashoffset: 48,
|
||||||
|
animation: (props:StyledProps) => `$stroke-keyframe ${props.animationDuration * 0.5}s
|
||||||
|
cubic-bezier(0.65, 0, 0.45, 1) 0.8s forwards`,
|
||||||
|
},
|
||||||
|
|
||||||
|
"@keyframes scale": {
|
||||||
|
"0%": {},
|
||||||
|
"100%": {
|
||||||
|
transform: "none",
|
||||||
|
},
|
||||||
|
"50%": {
|
||||||
|
transform: (props:StyledProps) => `scale3d(${props.explosion}, ${props.explosion}, 1)`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"@keyframes fill": {
|
||||||
|
"100%": {
|
||||||
|
boxShadow: (props:StyledProps) => `inset 0 0 0 100vh ${props.backgroundColor || theme.palette.success.main}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"@keyframes stroke-keyframe": {
|
||||||
|
"100%": {
|
||||||
|
/* this is needed because makeStyles function has bugs (https://github.com/mui-org/material-ui/issues/15511) */
|
||||||
|
strokeDashoffset:() => 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
));
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The different preset checkmark sizes. Measured in pixels
|
||||||
|
*/
|
||||||
|
export const sizes = {
|
||||||
|
xs: 12,
|
||||||
|
sm: 16,
|
||||||
|
md: 24,
|
||||||
|
lg: 52,
|
||||||
|
xl: 72,
|
||||||
|
xxl: 96,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Sizes = keyof typeof sizes;
|
||||||
|
|
||||||
|
export interface AnimatedCheckmarkProps {
|
||||||
|
/**
|
||||||
|
* Duration of the checkmark animation in milliseconds
|
||||||
|
*/
|
||||||
|
animationDuration: number;
|
||||||
|
/** A string for describing what the component means in screen readers*/
|
||||||
|
ariaLabel: string;
|
||||||
|
|
||||||
|
/** Color in hex of the circle and background of component
|
||||||
|
* Defaults to `theme.palette.success.main`
|
||||||
|
*/
|
||||||
|
backgroundColor?: string;
|
||||||
|
/**
|
||||||
|
* Color in hex of checkmark in the middle of the component
|
||||||
|
* Defaults to white (#000)
|
||||||
|
*/
|
||||||
|
checkColor?: string;
|
||||||
|
/**
|
||||||
|
* The stroke width of the checkmark. Defaults to 5.
|
||||||
|
*/
|
||||||
|
checkThickness: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How much the circle temporarily expands to on success. 1 means no expansion, 1.1 means 10% expansion, etc.
|
||||||
|
* Defaults to 1.1
|
||||||
|
*/
|
||||||
|
explosion: number;
|
||||||
|
/**
|
||||||
|
* The role for accessibility purposes.
|
||||||
|
* Defaults to 'alert'.
|
||||||
|
*/
|
||||||
|
role?: string;
|
||||||
|
/**
|
||||||
|
* The height and width in pixels of the component. Accepts a size string (listed in `sizes`) or a number
|
||||||
|
*
|
||||||
|
* Defaults to 'lg' which is 52 pixels
|
||||||
|
*/
|
||||||
|
size: Sizes | number;
|
||||||
|
/**
|
||||||
|
* Whether the component should be visible in the document.
|
||||||
|
*/
|
||||||
|
visible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AnimatedCheckmark(props: AnimatedCheckmarkProps): JSX.Element {
|
||||||
|
const selectedSize = typeof props.size === 'number' ? props.size : sizes[props.size];
|
||||||
|
|
||||||
|
|
||||||
|
const classes = useStyles({
|
||||||
|
backgroundColor: props.backgroundColor,
|
||||||
|
checkColor: props.checkColor,
|
||||||
|
checkThickness: props.checkThickness,
|
||||||
|
animationDuration: props.animationDuration,
|
||||||
|
explosion: props.explosion,
|
||||||
|
width: selectedSize,
|
||||||
|
height: selectedSize,
|
||||||
|
});
|
||||||
|
if (!props.visible) return <></>;
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
data-testid="CheckmarkTest"
|
||||||
|
className={classes.root}
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='0 0 52 52' role={props.role} aria-label={props.ariaLabel}
|
||||||
|
>
|
||||||
|
<circle className={classes.circle} cx='26' cy='26' r='25' fill='none' />
|
||||||
|
<path className={classes.checkmark} fill='none' d='M14.1 27.2l7.1 7.2 16.7-16.8' />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedCheckmark.defaultProps = {
|
||||||
|
size: 'lg',
|
||||||
|
visible: true,
|
||||||
|
checkColor: '#FFF',
|
||||||
|
checkThickness: 5,
|
||||||
|
animationDuration: 0.6,
|
||||||
|
explosion: 1.1,
|
||||||
|
role: 'alert',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AnimatedCheckmark;
|
Loading…
Reference in a new issue