mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 01:47:00 +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": {
|
"origin": {
|
||||||
"message": "Origin"
|
"message": "Origin"
|
||||||
},
|
},
|
||||||
|
"padlock": {
|
||||||
|
"message": "Padlock"
|
||||||
|
},
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"message": "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/index';
|
||||||
@import 'gas-details-item/gas-details-item-title/index';
|
@import 'gas-details-item/gas-details-item-title/index';
|
||||||
@import 'gas-timing/index';
|
@import 'gas-timing/index';
|
||||||
|
@import 'hold-to-reveal-button/index';
|
||||||
@import 'home-notification/index';
|
@import 'home-notification/index';
|
||||||
@import 'info-box/index';
|
@import 'info-box/index';
|
||||||
@import 'menu-bar/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