mirror of
https://github.com/elyby/accounts-frontend.git
synced 2025-05-31 14:11:58 +05:30
Create app namespace for all absolute requires of app modules. Move all packages under packages yarn workspace
This commit is contained in:
189
packages/app/components/ui/popup/PopupStack.test.js
Normal file
189
packages/app/components/ui/popup/PopupStack.test.js
Normal file
@@ -0,0 +1,189 @@
|
||||
import sinon from 'sinon';
|
||||
import expect from 'app/test/unexpected';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallow, mount } from 'enzyme';
|
||||
|
||||
import { PopupStack } from 'app/components/ui/popup/PopupStack';
|
||||
import styles from 'app/components/ui/popup/popup.scss';
|
||||
|
||||
function DummyPopup(/** @type {{[key: string]: any}} */ _props) {
|
||||
return null;
|
||||
}
|
||||
|
||||
describe('<PopupStack />', () => {
|
||||
it('renders all popup components', () => {
|
||||
const props = {
|
||||
destroy: () => {},
|
||||
popups: [
|
||||
{
|
||||
Popup: DummyPopup,
|
||||
},
|
||||
{
|
||||
Popup: DummyPopup,
|
||||
},
|
||||
],
|
||||
};
|
||||
const component = shallow(<PopupStack {...props} />);
|
||||
|
||||
expect(component.find(DummyPopup), 'to satisfy', { length: 2 });
|
||||
});
|
||||
|
||||
it('should pass onClose as props', () => {
|
||||
const expectedProps = {
|
||||
foo: 'bar',
|
||||
};
|
||||
|
||||
const props = {
|
||||
destroy: () => {},
|
||||
popups: [
|
||||
{
|
||||
Popup: (props = {}) => {
|
||||
// eslint-disable-next-line
|
||||
expect(props.onClose, 'to be a', 'function');
|
||||
|
||||
return <DummyPopup {...expectedProps} />;
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const component = mount(<PopupStack {...props} />);
|
||||
|
||||
const popup = component.find(DummyPopup);
|
||||
expect(popup, 'to satisfy', { length: 1 });
|
||||
expect(popup.props(), 'to equal', expectedProps);
|
||||
});
|
||||
|
||||
it('should hide popup, when onClose called', () => {
|
||||
const props = {
|
||||
popups: [
|
||||
{
|
||||
Popup: DummyPopup,
|
||||
},
|
||||
{
|
||||
Popup: DummyPopup,
|
||||
},
|
||||
],
|
||||
destroy: sinon.stub().named('props.destroy'),
|
||||
};
|
||||
const component = shallow(<PopupStack {...props} />);
|
||||
|
||||
component
|
||||
.find(DummyPopup)
|
||||
.last()
|
||||
.prop('onClose')();
|
||||
|
||||
expect(props.destroy, 'was called once');
|
||||
expect(props.destroy, 'to have a call satisfying', [
|
||||
expect.it('to be', props.popups[1]),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should hide popup, when overlay clicked', () => {
|
||||
const preventDefault = sinon.stub().named('event.preventDefault');
|
||||
const props = {
|
||||
destroy: sinon.stub().named('props.destroy'),
|
||||
popups: [
|
||||
{
|
||||
Popup: DummyPopup,
|
||||
},
|
||||
],
|
||||
};
|
||||
const component = shallow(<PopupStack {...props} />);
|
||||
|
||||
const overlay = component.find(`.${styles.overlay}`);
|
||||
overlay.simulate('click', { target: 1, currentTarget: 1, preventDefault });
|
||||
|
||||
expect(props.destroy, 'was called once');
|
||||
expect(preventDefault, 'was called once');
|
||||
});
|
||||
|
||||
it('should hide popup on overlay click if disableOverlayClose', () => {
|
||||
const props = {
|
||||
destroy: sinon.stub().named('props.destroy'),
|
||||
popups: [
|
||||
{
|
||||
Popup: DummyPopup,
|
||||
disableOverlayClose: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
const component = shallow(<PopupStack {...props} />);
|
||||
|
||||
const overlay = component.find(`.${styles.overlay}`);
|
||||
overlay.simulate('click', {
|
||||
target: 1,
|
||||
currentTarget: 1,
|
||||
preventDefault() {},
|
||||
});
|
||||
|
||||
expect(props.destroy, 'was not called');
|
||||
});
|
||||
|
||||
it('should hide popup, when esc pressed', () => {
|
||||
const props = {
|
||||
destroy: sinon.stub().named('props.destroy'),
|
||||
popups: [
|
||||
{
|
||||
Popup: DummyPopup,
|
||||
},
|
||||
],
|
||||
};
|
||||
mount(<PopupStack {...props} />);
|
||||
|
||||
const event = new Event('keyup');
|
||||
// @ts-ignore
|
||||
event.which = 27;
|
||||
document.dispatchEvent(event);
|
||||
|
||||
expect(props.destroy, 'was called once');
|
||||
});
|
||||
|
||||
it('should hide first popup in stack if esc pressed', () => {
|
||||
const props = {
|
||||
destroy: sinon.stub().named('props.destroy'),
|
||||
popups: [
|
||||
{
|
||||
Popup() {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
{
|
||||
Popup: DummyPopup,
|
||||
},
|
||||
],
|
||||
};
|
||||
mount(<PopupStack {...props} />);
|
||||
|
||||
const event = new Event('keyup');
|
||||
// @ts-ignore
|
||||
event.which = 27;
|
||||
document.dispatchEvent(event);
|
||||
|
||||
expect(props.destroy, 'was called once');
|
||||
expect(props.destroy, 'to have a call satisfying', [
|
||||
expect.it('to be', props.popups[1]),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should NOT hide popup on esc pressed if disableOverlayClose', () => {
|
||||
const props = {
|
||||
destroy: sinon.stub().named('props.destroy'),
|
||||
popups: [
|
||||
{
|
||||
Popup: DummyPopup,
|
||||
disableOverlayClose: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
mount(<PopupStack {...props} />);
|
||||
|
||||
const event = new Event('keyup');
|
||||
// @ts-ignore
|
||||
event.which = 27;
|
||||
document.dispatchEvent(event);
|
||||
|
||||
expect(props.destroy, 'was not called');
|
||||
});
|
||||
});
|
104
packages/app/components/ui/popup/PopupStack.tsx
Normal file
104
packages/app/components/ui/popup/PopupStack.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import React from 'react';
|
||||
import { TransitionGroup, CSSTransition } from 'react-transition-group';
|
||||
import { browserHistory } from 'app/services/history';
|
||||
import { connect } from 'react-redux';
|
||||
import { RootState } from 'app/reducers';
|
||||
|
||||
import { PopupConfig } from './reducer';
|
||||
import { destroy } from './actions';
|
||||
import styles from './popup.scss';
|
||||
|
||||
export class PopupStack extends React.Component<{
|
||||
popups: PopupConfig[];
|
||||
destroy: (popup: PopupConfig) => void;
|
||||
}> {
|
||||
unlistenTransition: () => void;
|
||||
|
||||
componentWillMount() {
|
||||
document.addEventListener('keyup', this.onKeyPress);
|
||||
this.unlistenTransition = browserHistory.listen(this.onRouteLeave);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keyup', this.onKeyPress);
|
||||
this.unlistenTransition();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { popups } = this.props;
|
||||
|
||||
return (
|
||||
<TransitionGroup>
|
||||
{popups.map((popup, index) => {
|
||||
const { Popup } = popup;
|
||||
|
||||
return (
|
||||
<CSSTransition
|
||||
key={index}
|
||||
classNames={{
|
||||
enter: styles.trEnter,
|
||||
enterActive: styles.trEnterActive,
|
||||
exit: styles.trExit,
|
||||
exitActive: styles.trExitActive,
|
||||
}}
|
||||
timeout={500}
|
||||
>
|
||||
<div
|
||||
className={styles.overlay}
|
||||
onClick={this.onOverlayClick(popup)}
|
||||
>
|
||||
<Popup onClose={this.onClose(popup)} />
|
||||
</div>
|
||||
</CSSTransition>
|
||||
);
|
||||
})}
|
||||
</TransitionGroup>
|
||||
);
|
||||
}
|
||||
|
||||
onClose(popup: PopupConfig) {
|
||||
return this.props.destroy.bind(null, popup);
|
||||
}
|
||||
|
||||
onOverlayClick(popup: PopupConfig) {
|
||||
return (event: React.MouseEvent) => {
|
||||
if (event.target !== event.currentTarget || popup.disableOverlayClose) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
this.props.destroy(popup);
|
||||
};
|
||||
}
|
||||
|
||||
popStack() {
|
||||
const [popup] = this.props.popups.slice(-1);
|
||||
|
||||
if (popup && !popup.disableOverlayClose) {
|
||||
this.props.destroy(popup);
|
||||
}
|
||||
}
|
||||
|
||||
onKeyPress = (event: KeyboardEvent) => {
|
||||
if (event.which === 27) {
|
||||
// ESC key
|
||||
this.popStack();
|
||||
}
|
||||
};
|
||||
|
||||
onRouteLeave = nextLocation => {
|
||||
if (nextLocation) {
|
||||
this.popStack();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: RootState) => ({
|
||||
...state.popup,
|
||||
}),
|
||||
{
|
||||
destroy,
|
||||
},
|
||||
)(PopupStack);
|
17
packages/app/components/ui/popup/actions.ts
Normal file
17
packages/app/components/ui/popup/actions.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { PopupConfig } from './reducer';
|
||||
|
||||
export const POPUP_CREATE = 'POPUP_CREATE';
|
||||
export function create(payload: PopupConfig) {
|
||||
return {
|
||||
type: POPUP_CREATE,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
export const POPUP_DESTROY = 'POPUP_DESTROY';
|
||||
export function destroy(popup: PopupConfig) {
|
||||
return {
|
||||
type: POPUP_DESTROY,
|
||||
payload: popup,
|
||||
};
|
||||
}
|
174
packages/app/components/ui/popup/popup.scss
Normal file
174
packages/app/components/ui/popup/popup.scss
Normal file
@@ -0,0 +1,174 @@
|
||||
@import '~app/components/ui/colors.scss';
|
||||
@import '~app/components/ui/fonts.scss';
|
||||
|
||||
$popupPadding: 20px; // Отступ контента внутри попапа
|
||||
$popupMargin: 20px; // Отступ попапа от краёв окна
|
||||
|
||||
@mixin popupBounding($width, $padding: null) {
|
||||
@if ($padding == null) {
|
||||
$padding: $popupMargin;
|
||||
}
|
||||
|
||||
@if ($padding != $popupMargin) {
|
||||
margin: $padding auto;
|
||||
padding: 0 $padding;
|
||||
}
|
||||
|
||||
max-width: $width;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
z-index: 200;
|
||||
|
||||
background: rgba(#fff, 0.8);
|
||||
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
perspective: 600px;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
height: 100%;
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.popupWrapper {
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto;
|
||||
padding: $popupMargin;
|
||||
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
|
||||
transition: max-width 0.3s;
|
||||
}
|
||||
|
||||
.popup {
|
||||
white-space: normal;
|
||||
text-align: left;
|
||||
|
||||
background: #fff;
|
||||
box-shadow: 0 0 10px rgba(#000, 0.2);
|
||||
|
||||
color: #666;
|
||||
|
||||
:invalid {
|
||||
button {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
background: $white;
|
||||
padding: 15px $popupPadding;
|
||||
border-bottom: 1px solid #dedede;
|
||||
}
|
||||
|
||||
.headerTitle {
|
||||
font-size: 20px;
|
||||
font-family: $font-family-title;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: $popupPadding;
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
padding: 15px;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
color: rgba(#000, 0.4);
|
||||
background: rgba(#000, 0);
|
||||
transition: 0.25s;
|
||||
transform: translateX(0);
|
||||
|
||||
&:hover {
|
||||
color: rgba(#000, 0.6);
|
||||
background: rgba(#fff, 0.75);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 655px) {
|
||||
.close {
|
||||
position: fixed;
|
||||
padding: 35px;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transition states
|
||||
*/
|
||||
$popupInitPosition: translateY(10%) rotateX(-8deg);
|
||||
|
||||
.trEnter {
|
||||
opacity: 0;
|
||||
|
||||
.popup {
|
||||
opacity: 0;
|
||||
transform: $popupInitPosition;
|
||||
}
|
||||
|
||||
&Active {
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s ease-in;
|
||||
|
||||
.popup {
|
||||
opacity: 1;
|
||||
transform: translateY(0) rotateX(0deg);
|
||||
transition: 0.3s cubic-bezier(0.23, 1, 0.32, 1); // easeOutQuint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.trExit {
|
||||
opacity: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.popup {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
&Active {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in;
|
||||
|
||||
.popup {
|
||||
opacity: 0;
|
||||
transform: $popupInitPosition;
|
||||
transition: 0.3s cubic-bezier(0.165, 0.84, 0.44, 1); // easeOutQuart
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.trEnter,
|
||||
.trExit {
|
||||
.close {
|
||||
// do not show close during transition, because transform forces position: fixed
|
||||
// to layout relative container, instead of body
|
||||
opacity: 0;
|
||||
transform: translate(100%);
|
||||
transition: 0s;
|
||||
}
|
||||
}
|
98
packages/app/components/ui/popup/reducer.test.js
Normal file
98
packages/app/components/ui/popup/reducer.test.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import expect from 'app/test/unexpected';
|
||||
import React from 'react';
|
||||
import reducer from 'app/components/ui/popup/reducer';
|
||||
import { create, destroy } from 'app/components/ui/popup/actions';
|
||||
|
||||
describe('popup/reducer', () => {
|
||||
it('should have no popups by default', () => {
|
||||
const actual = reducer(undefined, {
|
||||
type: 'init',
|
||||
});
|
||||
|
||||
expect(actual.popups, 'to be an', 'array');
|
||||
expect(actual.popups, 'to be empty');
|
||||
});
|
||||
|
||||
describe('#create', () => {
|
||||
it('should create popup', () => {
|
||||
const actual = reducer(
|
||||
undefined,
|
||||
create({
|
||||
Popup: FakeComponent,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(actual.popups[0], 'to equal', {
|
||||
Popup: FakeComponent,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create multiple popups', () => {
|
||||
let actual = reducer(
|
||||
undefined,
|
||||
create({
|
||||
Popup: FakeComponent,
|
||||
}),
|
||||
);
|
||||
actual = reducer(
|
||||
actual,
|
||||
create({
|
||||
Popup: FakeComponent,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(actual.popups[1], 'to equal', {
|
||||
Popup: FakeComponent,
|
||||
});
|
||||
});
|
||||
|
||||
it('throws when no type provided', () => {
|
||||
expect(
|
||||
() =>
|
||||
reducer(
|
||||
undefined,
|
||||
// @ts-ignore
|
||||
create({}),
|
||||
),
|
||||
'to throw',
|
||||
'Popup is required',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#destroy', () => {
|
||||
let state;
|
||||
let popup;
|
||||
|
||||
beforeEach(() => {
|
||||
state = reducer(state, create({ Popup: FakeComponent }));
|
||||
popup = state.popups[0];
|
||||
});
|
||||
|
||||
it('should remove popup', () => {
|
||||
expect(state.popups, 'to have length', 1);
|
||||
|
||||
state = reducer(state, destroy(popup));
|
||||
|
||||
expect(state.popups, 'to have length', 0);
|
||||
});
|
||||
|
||||
it('should not remove something, that it should not', () => {
|
||||
state = reducer(
|
||||
state,
|
||||
create({
|
||||
Popup: FakeComponent,
|
||||
}),
|
||||
);
|
||||
|
||||
state = reducer(state, destroy(popup));
|
||||
|
||||
expect(state.popups, 'to have length', 1);
|
||||
expect(state.popups[0], 'not to be', popup);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function FakeComponent() {
|
||||
return <span />;
|
||||
}
|
36
packages/app/components/ui/popup/reducer.ts
Normal file
36
packages/app/components/ui/popup/reducer.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import { POPUP_CREATE, POPUP_DESTROY } from './actions';
|
||||
|
||||
export interface PopupConfig {
|
||||
Popup: React.ElementType;
|
||||
props?: { [key: string]: any };
|
||||
// do not allow hiding popup
|
||||
disableOverlayClose?: boolean;
|
||||
}
|
||||
|
||||
export type State = {
|
||||
popups: PopupConfig[];
|
||||
};
|
||||
|
||||
export default combineReducers<State>({
|
||||
popups,
|
||||
});
|
||||
|
||||
function popups(state: PopupConfig[] = [], { type, payload }) {
|
||||
switch (type) {
|
||||
case POPUP_CREATE:
|
||||
if (!payload.Popup) {
|
||||
throw new Error('Popup is required');
|
||||
}
|
||||
|
||||
return state.concat(payload);
|
||||
|
||||
case POPUP_DESTROY:
|
||||
return state.filter(popup => popup !== payload);
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user