Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 19 additions & 12 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import PenDownToggleSwitch from './PenDownToggleSwitch';
import ProgramSpeedController from './ProgramSpeedController';
import { programIsEmpty } from './ProgramUtils';
import ProgramSerializer from './ProgramSerializer';
import ShareButton from './ShareButton';
import type { DeviceConnectionStatus, Program, RobotDriver } from './types';
import * as Utils from './Utils';
import messages from './messages.json';
Expand Down Expand Up @@ -580,23 +581,29 @@ export default class App extends React.Component<{}, AppState> {
/>
</Col>
</Row>
<div className='App__playControl-container'>
<div className='App__playButton-container'>
<PlayButton
interpreterIsRunning={this.state.interpreterIsRunning}
disabled={
this.state.interpreterIsRunning ||
programIsEmpty(this.state.program)}
onClick={this.handleClickPlay}
<div className='App__playAndShare-container'>
<div className='App__playControl-container'>
<div className='App__playButton-container'>
<PlayButton
interpreterIsRunning={this.state.interpreterIsRunning}
disabled={
this.state.interpreterIsRunning ||
programIsEmpty(this.state.program)}
onClick={this.handleClickPlay}
/>
</div>
<ProgramSpeedController
values={this.speedLookUp}
onChange={this.handleChangeProgramSpeed}
/>
</div>
<ProgramSpeedController
values={this.speedLookUp}
onChange={this.handleChangeProgramSpeed}
/>
<div className='App__shareButton-container'>
<ShareButton/>
</div>
</div>
</Container>
</div>

<DashConnectionErrorModal
show={this.state.showDashConnectionError}
onCancel={this.handleCancelDashConnection}
Expand Down
21 changes: 19 additions & 2 deletions src/App.scss
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,32 @@ body {
grid-gap: 1rem;
}

.App__playAndShare-container {
height: 10rem;
margin-top: 1rem;
background-color: #F1F1F1;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}

.App__playControl-container {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 1rem;
background-color: #F1F1F1;
padding: 0.5rem;
grid-column-start: 2;
}

.App__playButton-container {
margin-bottom: 1rem;
}

.App__shareButton-container {
align-items: flex-end;
display: flex;
flex-direction: column;
grid-column-start: 3;
grid-column-end: 4;
justify-content: center;
padding-right: 2rem;
}
3 changes: 3 additions & 0 deletions src/ShareButton.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.ShareButton {
width: 10rem;
}
73 changes: 73 additions & 0 deletions src/ShareButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// @flow

import React from 'react';
import {Button} from 'react-bootstrap';
import { injectIntl } from 'react-intl';
import type { IntlShape } from 'react-intl';

import ShareCompleteModal from './ShareCompleteModal';

import './ShareButton.css';

type ShareButtonProps = {
intl: IntlShape,
onShowModal?: Function
};

type ShareButtonState = {
showShareComplete: boolean
}

class ShareButton extends React.Component<ShareButtonProps, ShareButtonState> {
constructor (props: ShareButtonProps) {
super(props);
this.state = {
showShareComplete: false
}
}

handleClickShareButton = () => {
// Get the current URL, which represents the current program state.
const currentUrl = document.location.href;

// Copy the URL to the clipboard, see:
// https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText
navigator.clipboard.writeText(currentUrl).then(() => {
this.setState({ showShareComplete: true})
});
}

handleModalClose = () => {
this.setState({showShareComplete: false});
}

render() {
return (
<React.Fragment>
<Button
variant="dark"
className='ShareButton'
onClick={this.handleClickShareButton}
>
{this.props.intl.formatMessage({id:'ShareButton'})}
</Button>
<ShareCompleteModal
show={this.state.showShareComplete}
onHide={this.handleModalClose}
/>
</React.Fragment>
);
}

componentDidUpdate(prevProps: ShareButtonProps, prevState: ShareButtonState) {
if ((this.state.showShareComplete !== prevState.showShareComplete)
&& this.state.showShareComplete) {
if (this.props.onShowModal) {
this.props.onShowModal();
}
}
}
}

export default injectIntl(ShareButton);

108 changes: 108 additions & 0 deletions src/ShareButton.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// @flow
import React from 'react';
import ReactDOM from 'react-dom';
import Adapter from 'enzyme-adapter-react-16';
import { IntlProvider } from 'react-intl';
import { configure, shallow } from 'enzyme';
import { createIntl } from 'react-intl';
import messages from './messages.json';

import ShareButton from './ShareButton';

configure({ adapter: new Adapter()});

class FakeClipboard {
currentClipboardContents: string;

constructor() {
this.currentClipboardContents = '';
}

readText(): Promise<string> {
return Promise.resolve(this.currentClipboardContents);
}

writeText(data: string): Promise<void> {
this.currentClipboardContents = data;
return Promise.resolve();
}
}

const intl = createIntl({
locale: 'en',
defaultLocale: 'en',
messages: messages.en
});

function createShareButton(props) {
const wrapper = shallow(
React.createElement(
ShareButton.WrappedComponent,
Object.assign(
{},
{
intl: intl
},
props
)
)
);

return wrapper;
}

test('The component should render without errors.', () => {
const div = document.createElement('div');
ReactDOM.render(<IntlProvider
locale={intl.locale}
messages={intl.messages}
>
<ShareButton/>
</IntlProvider>, div);
ReactDOM.unmountComponentAtNode(div);
});

test('The modal should be hidden on startup', () => {
const wrapper = createShareButton();
const modal = wrapper.children().at(1);
expect(modal.props().show).toBe(false);
});

test('When the Share button is clicked, then the modal should be displayed', (done) => {
expect.assertions(1);

Object.assign(navigator, {
// $FlowFixMe: Flow wants us to mock the full clipboard before we do this.
clipboard: new FakeClipboard()
});

const wrapper = createShareButton({
// Register a callback to verify that the modal has been shown
onShowModal: () => {
const modal = wrapper.children().at(1);
expect(modal.props().show).toBe(true);
done();
}
});

const button = wrapper.children().at(0);
button.simulate("click");
});

test('When the Share button is clicked, then the URL should be copied to the clipboard', (done) => {
expect.assertions(1);

Object.assign(navigator, {
// $FlowFixMe: Flow wants us to mock the full clipboard before we do this.
clipboard: new FakeClipboard()
});

const wrapper = createShareButton();
const button = wrapper.children().at(0);
button.simulate("click");

navigator.clipboard.readText().then((clipBoardText) => {
expect(clipBoardText).toBe(document.location.href);
done();
});
});
8 changes: 8 additions & 0 deletions src/ShareCompleteModal.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.ShareCompleteModal {
border-radius: 0.25rem;
}

.ShareCompleteModal__content {
border-radius: 0.25rem;
background-color: yellow;
}
34 changes: 34 additions & 0 deletions src/ShareCompleteModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// @flow
import React from 'react';
import { Modal } from 'react-bootstrap';
import { injectIntl, FormattedMessage } from 'react-intl';
import type {IntlShape} from 'react-intl';

import './ShareCompleteModal.css';

type ShareCompleteModalProps = {
intl: IntlShape,
show: boolean,
onHide: Function
};

class ShareCompleteModal extends React.Component<ShareCompleteModalProps, {}> {
static defaultProps = {
show: false,
onHide: () => {}
}

render () {
return(<Modal
onHide={this.props.onHide}
show={this.props.show}
dialogClassName='ShareCompleteModal'
>
<Modal.Body className='ShareCompleteModal__content'>
<FormattedMessage id='ShareCompleteModal.shareComplete' />
</Modal.Body>
</Modal>);
}
}

export default injectIntl(ShareCompleteModal);
2 changes: 2 additions & 0 deletions src/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
"ProgramSpeedController.slider": "Program play speed",
"AudioFeedbackToggleSwitch.audioFeedback": "Audio feedback",
"Scene": "Scene",
"ShareButton": "Share",
"ShareCompleteModal.shareComplete": "The URL for your program has been copied to the clipboard.",
"RefreshButton": "Refresh"
},
"fr": {
Expand Down