Create app namespace for all absolute requires of app modules. Move all packages under packages yarn workspace

This commit is contained in:
SleepWalker
2019-12-07 21:02:00 +02:00
parent d8d2df0702
commit f9d3bb4e20
404 changed files with 758 additions and 742 deletions

View 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');
});
});

View 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);

View 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,
};
}

View 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;
}
}

View 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 />;
}

View 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;
}
}