mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-21 17:37:01 +01:00
HoldToRevealButton component (#13785)
* Created 'HoldToRevealButton' component * Added new line within the .svg files * Lint fix * CSS fix according to BEM * Modified unit test
This commit is contained in:
parent
40095cce67
commit
52b043c4f2
3
app/_locales/en/messages.json
generated
3
app/_locales/en/messages.json
generated
@ -2330,6 +2330,9 @@
|
||||
"origin": {
|
||||
"message": "Origin"
|
||||
},
|
||||
"padlock": {
|
||||
"message": "Padlock"
|
||||
},
|
||||
"parameters": {
|
||||
"message": "Parameters"
|
||||
},
|
||||
|
3
app/images/lock-icon.svg
Normal file
3
app/images/lock-icon.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="8" height="9" viewBox="0 0 8 9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.03125 3.9375H6.60938V2.67188C6.60938 1.19883 5.41055 0 3.9375 0C2.46445 0 1.26562 1.19883 1.26562 2.67188V3.9375H0.84375C0.37793 3.9375 0 4.31543 0 4.78125V8.15625C0 8.62207 0.37793 9 0.84375 9H7.03125C7.49707 9 7.875 8.62207 7.875 8.15625V4.78125C7.875 4.31543 7.49707 3.9375 7.03125 3.9375ZM5.20312 3.9375H2.67188V2.67188C2.67188 1.97402 3.23965 1.40625 3.9375 1.40625C4.63535 1.40625 5.20312 1.97402 5.20312 2.67188V3.9375Z" fill="#EAF6FF"/>
|
||||
</svg>
|
After Width: | Height: | Size: 556 B |
3
app/images/unlock-icon.svg
Normal file
3
app/images/unlock-icon.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="7" height="9" viewBox="0 0 7 9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.25 4.50001H2.375V2.68771C2.375 1.99162 2.87031 1.4133 3.48906 1.40626C4.11406 1.39923 4.625 1.97052 4.625 2.67189V2.95314C4.625 3.18693 4.79219 3.37501 5 3.37501H5.5C5.70781 3.37501 5.875 3.18693 5.875 2.95314V2.67189C5.875 1.19533 4.80469 -0.00525612 3.49219 1.7306e-05C2.17969 0.00529073 1.125 1.22169 1.125 2.69825V4.50001H0.75C0.335938 4.50001 0 4.87794 0 5.34376V8.15625C0 8.62207 0.335938 9 0.75 9H6.25C6.66406 9 7 8.62207 7 8.15625V5.34376C7 4.87794 6.66406 4.50001 6.25 4.50001Z" fill="#EAF6FF"/>
|
||||
</svg>
|
After Width: | Height: | Size: 616 B |
@ -40,6 +40,7 @@
|
||||
@import 'gas-details-item/index';
|
||||
@import 'gas-details-item/gas-details-item-title/index';
|
||||
@import 'gas-timing/index';
|
||||
@import 'hold-to-reveal-button/index';
|
||||
@import 'home-notification/index';
|
||||
@import 'info-box/index';
|
||||
@import 'menu-bar/index';
|
||||
|
192
ui/components/app/hold-to-reveal-button/hold-to-reveal-button.js
Normal file
192
ui/components/app/hold-to-reveal-button/hold-to-reveal-button.js
Normal file
@ -0,0 +1,192 @@
|
||||
import React, { useCallback, useContext, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Button from '../../ui/button';
|
||||
import { I18nContext } from '../../../contexts/i18n';
|
||||
import Box from '../../ui/box/box';
|
||||
import {
|
||||
ALIGN_ITEMS,
|
||||
DISPLAY,
|
||||
JUSTIFY_CONTENT,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
|
||||
const radius = 14;
|
||||
const strokeWidth = 2;
|
||||
const radiusWithStroke = radius - strokeWidth / 2;
|
||||
|
||||
export default function HoldToRevealButton({ buttonText, onLongPressed }) {
|
||||
const t = useContext(I18nContext);
|
||||
const isLongPressing = useRef(false);
|
||||
const [isUnlocking, setIsUnlocking] = useState(false);
|
||||
const [hasTriggeredUnlock, setHasTriggeredUnlock] = useState(false);
|
||||
|
||||
/**
|
||||
* Prevent animation events from propogating up
|
||||
*
|
||||
* @param e - Native animation event - React.AnimationEvent<HTMLDivElement>
|
||||
*/
|
||||
const preventPropogation = (e) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
/**
|
||||
* Event for mouse click down
|
||||
*/
|
||||
const onMouseDown = () => {
|
||||
isLongPressing.current = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Event for mouse click up
|
||||
*/
|
||||
const onMouseUp = () => {
|
||||
isLongPressing.current = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 1. Progress cirle completed. Begin next animation phase (Shrink halo and show unlocked padlock)
|
||||
*/
|
||||
const onProgressComplete = () => {
|
||||
isLongPressing.current && setIsUnlocking(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* 2. Trigger onLongPressed callback. Begin next animation phase (Shrink unlocked padlock and fade in original content)
|
||||
*
|
||||
* @param e - Native animation event - React.AnimationEvent<HTMLDivElement>
|
||||
*/
|
||||
const triggerOnLongPressed = (e) => {
|
||||
onLongPressed();
|
||||
setHasTriggeredUnlock(true);
|
||||
preventPropogation(e);
|
||||
};
|
||||
|
||||
/**
|
||||
* 3. Reset animation states
|
||||
*/
|
||||
const resetAnimationStates = () => {
|
||||
setIsUnlocking(false);
|
||||
setHasTriggeredUnlock(false);
|
||||
};
|
||||
|
||||
const renderPreCompleteContent = useCallback(() => {
|
||||
return (
|
||||
<Box
|
||||
className={`hold-to-reveal-button__absolute-fill ${
|
||||
isUnlocking ? 'hold-to-reveal-button__invisible' : null
|
||||
} ${
|
||||
hasTriggeredUnlock ? 'hold-to-reveal-button__main-icon-show' : null
|
||||
}`}
|
||||
>
|
||||
<Box className="hold-to-reveal-button__absolute-fill">
|
||||
<svg className="hold-to-reveal-button__circle-svg">
|
||||
<circle
|
||||
className="hold-to-reveal-button__circle-background"
|
||||
cx={radius}
|
||||
cy={radius}
|
||||
r={radiusWithStroke}
|
||||
/>
|
||||
</svg>
|
||||
</Box>
|
||||
<Box className="hold-to-reveal-button__absolute-fill">
|
||||
<svg className="hold-to-reveal-button__circle-svg">
|
||||
<circle
|
||||
onTransitionEnd={onProgressComplete}
|
||||
className="hold-to-reveal-button__circle-foreground"
|
||||
cx={radius}
|
||||
cy={radius}
|
||||
r={radiusWithStroke}
|
||||
/>
|
||||
</svg>
|
||||
</Box>
|
||||
<Box
|
||||
display={DISPLAY.FLEX}
|
||||
alignItems={ALIGN_ITEMS.CENTER}
|
||||
justifyContent={JUSTIFY_CONTENT.CENTER}
|
||||
className="hold-to-reveal-button__lock-icon-container"
|
||||
>
|
||||
<img
|
||||
src="images/lock-icon.svg"
|
||||
alt={t('padlock')}
|
||||
className="hold-to-reveal-button__lock-icon"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}, [isUnlocking, hasTriggeredUnlock, t]);
|
||||
|
||||
const renderPostCompleteContent = useCallback(() => {
|
||||
return isUnlocking ? (
|
||||
<div
|
||||
className={`hold-to-reveal-button__absolute-fill ${
|
||||
hasTriggeredUnlock ? 'hold-to-reveal-button__unlock-icon-hide' : null
|
||||
}`}
|
||||
onAnimationEnd={resetAnimationStates}
|
||||
>
|
||||
<div
|
||||
onAnimationEnd={preventPropogation}
|
||||
className="hold-to-reveal-button__absolute-fill hold-to-reveal-button__circle-static-outer-container"
|
||||
>
|
||||
<svg className="hold-to-reveal-button__circle-svg">
|
||||
<circle
|
||||
className="hold-to-reveal-button__circle-static-outer"
|
||||
cx={14}
|
||||
cy={14}
|
||||
r={14}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
onAnimationEnd={preventPropogation}
|
||||
className="hold-to-reveal-button__absolute-fill hold-to-reveal-button__circle-static-inner-container"
|
||||
>
|
||||
<svg className="hold-to-reveal-button__circle-svg">
|
||||
<circle
|
||||
className="hold-to-reveal-button__circle-static-inner"
|
||||
cx={14}
|
||||
cy={14}
|
||||
r={12}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
className="hold-to-reveal-button__unlock-icon-container"
|
||||
onAnimationEnd={triggerOnLongPressed}
|
||||
>
|
||||
<img
|
||||
src="images/unlock-icon.svg"
|
||||
alt={t('padlock')}
|
||||
className="hold-to-reveal-button__unlock-icon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
}, [isUnlocking, hasTriggeredUnlock, t]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseUp={onMouseUp}
|
||||
type="primary"
|
||||
icon={
|
||||
<Box marginRight={2} className="hold-to-reveal-button__icon-container">
|
||||
{renderPreCompleteContent()}
|
||||
{renderPostCompleteContent()}
|
||||
</Box>
|
||||
}
|
||||
className="hold-to-reveal-button__button-hold"
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
HoldToRevealButton.propTypes = {
|
||||
/**
|
||||
* Text to be displayed on the button
|
||||
*/
|
||||
buttonText: PropTypes.string.isRequired,
|
||||
/**
|
||||
* Function to be called after the animation is finished
|
||||
*/
|
||||
onLongPressed: PropTypes.func.isRequired,
|
||||
};
|
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import HoldToRevealButton from './hold-to-reveal-button';
|
||||
|
||||
export default {
|
||||
title: 'Components/App/HoldToRevealButton',
|
||||
id: __filename,
|
||||
argTypes: {
|
||||
buttonText: { control: 'text' },
|
||||
onLongPressed: { action: 'Revealing the SRP' },
|
||||
},
|
||||
};
|
||||
|
||||
export const DefaultStory = (args) => {
|
||||
return <HoldToRevealButton {...args} />;
|
||||
};
|
||||
|
||||
DefaultStory.storyName = 'Default';
|
||||
|
||||
DefaultStory.args = {
|
||||
buttonText: 'Hold to reveal SRP',
|
||||
onLongPressed: () => console.log('Revealed'),
|
||||
};
|
@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { render, fireEvent, waitFor } from '@testing-library/react';
|
||||
import HoldToRevealButton from './hold-to-reveal-button';
|
||||
|
||||
describe('HoldToRevealButton', () => {
|
||||
let props = {};
|
||||
|
||||
beforeEach(() => {
|
||||
const mockOnLongPressed = jest.fn();
|
||||
|
||||
props = {
|
||||
onLongPressed: mockOnLongPressed,
|
||||
buttonText: 'Hold to reveal SRP',
|
||||
};
|
||||
});
|
||||
|
||||
it('should render a button with label', () => {
|
||||
const { getByText } = render(<HoldToRevealButton {...props} />);
|
||||
|
||||
expect(getByText('Hold to reveal SRP')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render a button when mouse is down and up', () => {
|
||||
const { getByText } = render(<HoldToRevealButton {...props} />);
|
||||
|
||||
const button = getByText('Hold to reveal SRP');
|
||||
|
||||
fireEvent.mouseDown(button);
|
||||
|
||||
expect(button).toBeDefined();
|
||||
|
||||
fireEvent.mouseUp(button);
|
||||
|
||||
expect(button).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not show the locked padlock when a button is long pressed and then should show it after it was lifted off before the animation concludes', () => {
|
||||
const { getByText } = render(<HoldToRevealButton {...props} />);
|
||||
|
||||
const button = getByText('Hold to reveal SRP');
|
||||
|
||||
fireEvent.mouseDown(button);
|
||||
|
||||
waitFor(() => {
|
||||
expect(button.firstChild).toHaveClass(
|
||||
'hold-to-reveal-button__lock-icon-container',
|
||||
);
|
||||
});
|
||||
|
||||
fireEvent.mouseUp(button);
|
||||
|
||||
waitFor(() => {
|
||||
expect(button.firstChild).not.toHaveClass(
|
||||
'hold-to-reveal-button__lock-icon-container',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should show the unlocked padlock when a button is long pressed for the duration of the animation', () => {
|
||||
const { getByText } = render(<HoldToRevealButton {...props} />);
|
||||
|
||||
const button = getByText('Hold to reveal SRP');
|
||||
|
||||
fireEvent.mouseDown(button);
|
||||
|
||||
waitFor(() => {
|
||||
expect(button.firstChild).toHaveClass(
|
||||
'hold-to-reveal-button__unlock-icon-container',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
1
ui/components/app/hold-to-reveal-button/index.js
Normal file
1
ui/components/app/hold-to-reveal-button/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './hold-to-reveal-button';
|
164
ui/components/app/hold-to-reveal-button/index.scss
Normal file
164
ui/components/app/hold-to-reveal-button/index.scss
Normal file
@ -0,0 +1,164 @@
|
||||
// Variables
|
||||
$circle-radius: 14px;
|
||||
$circle-diameter: $circle-radius * 2;
|
||||
// Circumference ~ (2*PI*R). We reduced the number a little to create a snappier interaction
|
||||
$circle-circumference: 82;
|
||||
$circle-stroke-width: 2px;
|
||||
|
||||
// Keyframes
|
||||
@keyframes collapse {
|
||||
from {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes expand {
|
||||
from {
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.hold-to-reveal-button {
|
||||
// Shared styles
|
||||
&__absolute-fill {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
height: $circle-diameter;
|
||||
width: $circle-diameter;
|
||||
}
|
||||
|
||||
&__circle-shared {
|
||||
fill: transparent;
|
||||
stroke-width: $circle-stroke-width;
|
||||
}
|
||||
|
||||
// Class styles
|
||||
&__button-hold {
|
||||
padding: 6px 13px 6px 9px !important;
|
||||
transform: scale(1) !important;
|
||||
transition: 0.5s transform !important;
|
||||
|
||||
&:active {
|
||||
background-color: var(--primary-1) !important;
|
||||
transform: scale(1.05) !important;
|
||||
|
||||
.hold-to-reveal-button__circle-foreground {
|
||||
stroke-dashoffset: 0 !important;
|
||||
}
|
||||
|
||||
.hold-to-reveal-button__lock-icon-container {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__absolute-fill {
|
||||
@extend .hold-to-reveal-button__absolute-fill;
|
||||
}
|
||||
|
||||
&__icon-container {
|
||||
@extend .hold-to-reveal-button__icon;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__main-icon-show {
|
||||
animation: 0.4s fadeIn 1.2s forwards;
|
||||
}
|
||||
|
||||
&__invisible {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&__circle-svg {
|
||||
@extend .hold-to-reveal-button__icon;
|
||||
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
&__circle-background {
|
||||
@extend .hold-to-reveal-button__circle-shared;
|
||||
|
||||
stroke: var(--primary-3);
|
||||
}
|
||||
|
||||
&__circle-foreground {
|
||||
@extend .hold-to-reveal-button__circle-shared;
|
||||
|
||||
stroke: var(--ui-white);
|
||||
stroke-dasharray: $circle-circumference;
|
||||
stroke-dashoffset: $circle-circumference;
|
||||
transition: 1s stroke-dashoffset;
|
||||
}
|
||||
|
||||
&__lock-icon-container {
|
||||
@extend .hold-to-reveal-button__absolute-fill;
|
||||
|
||||
transition: 0.3s opacity;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&__lock-icon {
|
||||
width: 7.88px;
|
||||
height: 9px;
|
||||
}
|
||||
|
||||
&__unlock-icon-hide {
|
||||
animation: 0.3s collapse 1s forwards;
|
||||
}
|
||||
|
||||
&__circle-static-outer-container {
|
||||
animation: 0.25s collapse forwards;
|
||||
}
|
||||
|
||||
&__circle-static-outer {
|
||||
fill: var(--ui-white);
|
||||
}
|
||||
|
||||
&__circle-static-inner-container {
|
||||
animation: 0.125s collapse forwards;
|
||||
}
|
||||
|
||||
&__circle-static-inner {
|
||||
fill: var(--primary-1);
|
||||
}
|
||||
|
||||
&__unlock-icon-container {
|
||||
@extend .hold-to-reveal-button__absolute-fill;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: scale(0);
|
||||
animation: 0.175s expand 0.2s forwards;
|
||||
}
|
||||
|
||||
&__unlock-icon {
|
||||
width: 14px;
|
||||
height: 11px;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user