mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-23 15:47:53 -05:00
Merge pull request #4482 from picklesrus/m-modal-draft
Initial skeleton of mute modal.
This commit is contained in:
commit
966df020f2
5 changed files with 372 additions and 0 deletions
115
src/components/modal/mute/modal.jsx
Normal file
115
src/components/modal/mute/modal.jsx
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
const bindAll = require('lodash.bindall');
|
||||||
|
const PropTypes = require('prop-types');
|
||||||
|
const React = require('react');
|
||||||
|
const Modal = require('../base/modal.jsx');
|
||||||
|
const ModalInnerContent = require('../base/modal-inner-content.jsx');
|
||||||
|
const Button = require('../../forms/button.jsx');
|
||||||
|
const Progression = require('../../progression/progression.jsx');
|
||||||
|
const FlexRow = require('../../flex-row/flex-row.jsx');
|
||||||
|
const MuteStep = require('./mute-step.jsx');
|
||||||
|
const classNames = require('classnames');
|
||||||
|
require('./modal.scss');
|
||||||
|
|
||||||
|
class MuteModal extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
bindAll(this, [
|
||||||
|
'handleNext',
|
||||||
|
'handlePrevious'
|
||||||
|
]);
|
||||||
|
this.state = {
|
||||||
|
step: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
handleNext () {
|
||||||
|
this.setState({
|
||||||
|
step: this.state.step + 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
handlePrevious () {
|
||||||
|
// This shouldn't get called when we're on the first step, but
|
||||||
|
// the Math.max is here as a safeguard so state doesn't go negative.
|
||||||
|
this.setState({
|
||||||
|
step: Math.max(0, this.state.step - 1)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen
|
||||||
|
useStandardSizes
|
||||||
|
className="modal-mute"
|
||||||
|
showCloseButton={false}
|
||||||
|
onRequestClose={this.props.onRequestClose}
|
||||||
|
>
|
||||||
|
<div className="mute-modal-header modal-header" />
|
||||||
|
<ModalInnerContent className="mute-inner-content">
|
||||||
|
<Progression step={this.state.step}>
|
||||||
|
<MuteStep
|
||||||
|
bottomImg="/images/bottom_placeholder.png"
|
||||||
|
bottomImgClass="bottom-img"
|
||||||
|
header="The Scratch comment filter thinks your comment was unconstructive."
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
If you think something could be better, you can say something you like about the project,
|
||||||
|
and make a suggestion about how to improve it. For example, you could say:
|
||||||
|
</p>
|
||||||
|
</MuteStep>
|
||||||
|
<MuteStep
|
||||||
|
header="For the next X minutes you won't be able to post comments"
|
||||||
|
sideImg="/images/side_placeholder.png"
|
||||||
|
sideImgClass="side-img"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Once X minutes have passed, you will be able to comment again.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you would like more information, you can read the Scratch community guidelines.
|
||||||
|
</p>
|
||||||
|
</MuteStep>
|
||||||
|
</Progression>
|
||||||
|
<FlexRow className={classNames('nav-divider')} />
|
||||||
|
<FlexRow className={classNames('mute-nav')}>
|
||||||
|
{this.state.step > 0 ? (
|
||||||
|
<Button
|
||||||
|
className={classNames(
|
||||||
|
'back-button',
|
||||||
|
)}
|
||||||
|
onClick={this.handlePrevious}
|
||||||
|
>
|
||||||
|
<div className="action-button-text">
|
||||||
|
Back
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
) : null }
|
||||||
|
{this.state.step >= 1 ? (
|
||||||
|
<Button
|
||||||
|
className={classNames('close-button')}
|
||||||
|
onClick={this.props.onRequestClose}
|
||||||
|
>
|
||||||
|
<div className="action-button-text">
|
||||||
|
Close
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
className={classNames('next-button')}
|
||||||
|
onClick={this.handleNext}
|
||||||
|
>
|
||||||
|
<div className="action-button-text">
|
||||||
|
Next
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</FlexRow>
|
||||||
|
</ModalInnerContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MuteModal.propTypes = {
|
||||||
|
onRequestClose: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = MuteModal;
|
60
src/components/modal/mute/modal.scss
Normal file
60
src/components/modal/mute/modal.scss
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
@import "../../../colors";
|
||||||
|
@import "../../../frameless";
|
||||||
|
|
||||||
|
.modal-mute {
|
||||||
|
width: 30rem;
|
||||||
|
|
||||||
|
.mute-modal-header {
|
||||||
|
box-shadow: inset 0 -1px 0 0 $ui-mint-green;
|
||||||
|
background-color: $ui-mint-green;
|
||||||
|
border-radius: 1rem 1rem 0 0;
|
||||||
|
}
|
||||||
|
.mute-step {
|
||||||
|
display: flex;
|
||||||
|
padding: 48px 16px;
|
||||||
|
}
|
||||||
|
.mute-content {
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
.mute-inner-content {
|
||||||
|
padding: 0 32px;
|
||||||
|
}
|
||||||
|
.left-column {
|
||||||
|
padding-right: 32px;
|
||||||
|
}
|
||||||
|
.mute-header {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 2rem;
|
||||||
|
}
|
||||||
|
.mute-bottom-row {
|
||||||
|
padding-top: 32px;
|
||||||
|
}
|
||||||
|
.bottom-img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.mute-side-image {
|
||||||
|
margin-left: -49px;
|
||||||
|
}
|
||||||
|
.side-img {
|
||||||
|
height: 212px;
|
||||||
|
width: 129px;
|
||||||
|
}
|
||||||
|
.nav-divider {
|
||||||
|
border-top: 1px solid $ui-blue-25percent;
|
||||||
|
}
|
||||||
|
.mute-nav {
|
||||||
|
display:flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 24px 0;
|
||||||
|
}
|
||||||
|
.back-button {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.next-button, .close-button {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
55
src/components/modal/mute/mute-step.jsx
Normal file
55
src/components/modal/mute/mute-step.jsx
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
const PropTypes = require('prop-types');
|
||||||
|
const React = require('react');
|
||||||
|
const classNames = require('classnames');
|
||||||
|
|
||||||
|
const FlexRow = require('../../flex-row/flex-row.jsx');
|
||||||
|
require('./modal.scss');
|
||||||
|
|
||||||
|
const MuteStep = ({
|
||||||
|
bottomImg,
|
||||||
|
bottomImgClass,
|
||||||
|
children,
|
||||||
|
header,
|
||||||
|
sideImg,
|
||||||
|
sideImgClass
|
||||||
|
}) => (
|
||||||
|
<div className="mute-step">
|
||||||
|
{sideImg &&
|
||||||
|
<FlexRow className={classNames('left-column')}>
|
||||||
|
<div className={classNames('mute-side-image')}>
|
||||||
|
<img
|
||||||
|
className={sideImgClass}
|
||||||
|
src={sideImg}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FlexRow>
|
||||||
|
}
|
||||||
|
<FlexRow className={classNames('mute-right-column')}>
|
||||||
|
<FlexRow className={classNames('mute-header')}>
|
||||||
|
{header}
|
||||||
|
</FlexRow>
|
||||||
|
<FlexRow className={classNames('mute-content')}>
|
||||||
|
{children}
|
||||||
|
</FlexRow>
|
||||||
|
<FlexRow className={classNames('mute-bottom-row')}>
|
||||||
|
{bottomImg &&
|
||||||
|
<img
|
||||||
|
className={bottomImgClass}
|
||||||
|
src={bottomImg}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</FlexRow>
|
||||||
|
</FlexRow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
MuteStep.propTypes = {
|
||||||
|
bottomImg: PropTypes.string,
|
||||||
|
bottomImgClass: PropTypes.string,
|
||||||
|
children: PropTypes.node,
|
||||||
|
header: PropTypes.string,
|
||||||
|
sideImg: PropTypes.string,
|
||||||
|
sideImgClass: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = MuteStep;
|
93
test/unit/components/mute-modal.test.jsx
Normal file
93
test/unit/components/mute-modal.test.jsx
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {shallowWithIntl} from '../../helpers/intl-helpers.jsx';
|
||||||
|
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
|
||||||
|
import MuteModal from '../../../src/components/modal/mute/modal';
|
||||||
|
import Modal from '../../../src/components/modal/base/modal';
|
||||||
|
|
||||||
|
|
||||||
|
describe('MuteModalTest', () => {
|
||||||
|
|
||||||
|
test('Mute Modal rendering', () => {
|
||||||
|
const component = shallowWithIntl(
|
||||||
|
<MuteModal />
|
||||||
|
);
|
||||||
|
expect(component.find('div.mute-modal-header').exists()).toEqual(true);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Mute Modal only shows next button on initial step', () => {
|
||||||
|
const component = mountWithIntl(
|
||||||
|
<MuteModal />
|
||||||
|
);
|
||||||
|
expect(component.find('div.mute-nav').exists()).toEqual(true);
|
||||||
|
expect(component.find('button.next-button').exists()).toEqual(true);
|
||||||
|
expect(component.find('button.next-button').getElements()[0].props.onClick)
|
||||||
|
.toEqual(component.instance().handleNext);
|
||||||
|
expect(component.find('button.close-button').exists()).toEqual(false);
|
||||||
|
expect(component.find('button.back-button').exists()).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Mute Modal shows back & close button on last step', () => {
|
||||||
|
const component = mountWithIntl(
|
||||||
|
<MuteModal />
|
||||||
|
);
|
||||||
|
// Step 1 is the last step.
|
||||||
|
component.instance().setState({step: 1});
|
||||||
|
component.update();
|
||||||
|
|
||||||
|
expect(component.find('div.mute-nav').exists()).toEqual(true);
|
||||||
|
expect(component.find('button.next-button').exists()).toEqual(false);
|
||||||
|
expect(component.find('button.back-button').exists()).toEqual(true);
|
||||||
|
expect(component.find('button.back-button').getElements()[0].props.onClick)
|
||||||
|
.toEqual(component.instance().handlePrevious);
|
||||||
|
expect(component.find('button.close-button').exists()).toEqual(true);
|
||||||
|
expect(component.find('button.close-button').getElements()[0].props.onClick)
|
||||||
|
.toEqual(component.instance().props.onRequestClose);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Mute modal sends correct props to Modal', () => {
|
||||||
|
const closeFn = jest.fn();
|
||||||
|
const component = shallowWithIntl(
|
||||||
|
<MuteModal
|
||||||
|
onRequestClose={closeFn}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const modal = component.find(Modal);
|
||||||
|
expect(modal).toHaveLength(1);
|
||||||
|
expect(modal.props().showCloseButton).toBe(false);
|
||||||
|
expect(modal.props().isOpen).toBe(true);
|
||||||
|
expect(modal.props().className).toBe('modal-mute');
|
||||||
|
expect(modal.props().onRequestClose).toBe(closeFn);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Mute modal handle next step', () => {
|
||||||
|
const closeFn = jest.fn();
|
||||||
|
const component = shallowWithIntl(
|
||||||
|
<MuteModal
|
||||||
|
onRequestClose={closeFn}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(component.instance().state.step).toBe(0);
|
||||||
|
component.instance().handleNext();
|
||||||
|
expect(component.instance().state.step).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Mute modal handle previous step', () => {
|
||||||
|
const component = shallowWithIntl(
|
||||||
|
<MuteModal />
|
||||||
|
);
|
||||||
|
component.instance().setState({step: 1});
|
||||||
|
|
||||||
|
component.instance().handlePrevious();
|
||||||
|
expect(component.instance().state.step).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Mute modal handle previous step stops at 0', () => {
|
||||||
|
const component = shallowWithIntl(
|
||||||
|
<MuteModal />
|
||||||
|
);
|
||||||
|
component.instance().setState({step: 0});
|
||||||
|
component.instance().handlePrevious();
|
||||||
|
expect(component.instance().state.step).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
49
test/unit/components/mute-step.test.jsx
Normal file
49
test/unit/components/mute-step.test.jsx
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
|
||||||
|
import MuteStep from '../../../src/components/modal/mute/mute-step';
|
||||||
|
|
||||||
|
describe('MuteStepTest', () => {
|
||||||
|
test('Mute Step with no images ', () => {
|
||||||
|
const component = mountWithIntl(
|
||||||
|
<MuteStep
|
||||||
|
header="header text"
|
||||||
|
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(component.find('div.mute-step').exists()).toEqual(true);
|
||||||
|
expect(component.find('div.mute-header').exists()).toEqual(true);
|
||||||
|
expect(component.find('div.mute-right-column').exists()).toEqual(true);
|
||||||
|
// No images and no left column.
|
||||||
|
expect(component.find('img').exists()).toEqual(false);
|
||||||
|
expect(component.find('div.left-column').exists()).toEqual(false);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Mute Step with side image ', () => {
|
||||||
|
const component = mountWithIntl(
|
||||||
|
<MuteStep
|
||||||
|
sideImg="/path/to/img.png"
|
||||||
|
sideImgClass="side-img"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(component.find('div.mute-step').exists()).toEqual(true);
|
||||||
|
expect(component.find('div.mute-header').exists()).toEqual(true);
|
||||||
|
expect(component.find('div.mute-right-column').exists()).toEqual(true);
|
||||||
|
expect(component.find('div.left-column').exists()).toEqual(true);
|
||||||
|
expect(component.find('img.side-img').exists()).toEqual(true);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Mute Step with bottom image ', () => {
|
||||||
|
const component = mountWithIntl(
|
||||||
|
<MuteStep
|
||||||
|
bottomImg="/path/to/img.png"
|
||||||
|
bottomImgClass="bottom-image"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(component.find('div.mute-step').exists()).toEqual(true);
|
||||||
|
expect(component.find('div.mute-header').exists()).toEqual(true);
|
||||||
|
expect(component.find('div.mute-right-column').exists()).toEqual(true);
|
||||||
|
expect(component.find('img.bottom-image').exists()).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue