Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

InstructionsWithWorkspace owns the resizer #32417

Merged
merged 7 commits into from
Jan 6, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/src/templates/instructions/HeightResizer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const styles = {
main: {
position: 'absolute',
height: RESIZER_HEIGHT,
left: 0,
marginLeft: 15,
right: 0
},
ellipsis: {
Expand Down Expand Up @@ -102,6 +102,7 @@ class HeightResizer extends React.Component {
return (
<div
id="ui-test-resizer"
className="editor-column"
style={mainStyle}
onMouseDown={this.onMouseDown}
onMouseUp={this.onMouseUp}
Expand Down
58 changes: 54 additions & 4 deletions apps/src/templates/instructions/InstructionsWithWorkspace.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@ import PropTypes from 'prop-types';
import React from 'react';
import {connect} from 'react-redux';
import CodeWorkspaceContainer from '../CodeWorkspaceContainer';
import TopInstructions from './TopInstructions';
import {setInstructionsMaxHeightAvailable} from '../../redux/instructions';
import TopInstructions, {MIN_HEIGHT} from './TopInstructions';
import {
setInstructionsMaxHeightAvailable,
setInstructionsRenderedHeight
} from '../../redux/instructions';
import HeightResizer from './HeightResizer';
import clamp from 'lodash/clamp';

/**
* A component representing the right side of the screen in our app. In particular
Expand All @@ -18,7 +23,10 @@ export class UnwrappedInstructionsWithWorkspace extends React.Component {
children: PropTypes.node,
// props provided via connect
instructionsHeight: PropTypes.number.isRequired,
setInstructionsMaxHeightAvailable: PropTypes.func.isRequired
instructionsMaxHeight: PropTypes.number.isRequired,
isEmbedView: PropTypes.bool.isRequired,
setInstructionsMaxHeightAvailable: PropTypes.func.isRequired,
setInstructionsRenderedHeight: PropTypes.func.isRequired
};

// only used so that we can rerender when resized
Expand All @@ -36,6 +44,15 @@ export class UnwrappedInstructionsWithWorkspace extends React.Component {
* call adjustTopPaneHeight as our maxHeight may need adjusting.
*/
onResize = () => {
// We have to have a reference to this component to do anything on resize anyway.
// Guard here because our tests aren't cleaning up nicely :(
if (!this.codeWorkspaceContainer) {
return;
}

// TODO (brad)
// See if we can achieve this effect with memoization instead of state
// https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#what-about-memoization
const {
windowWidth: lastWindowWidth,
windowHeight: lastWindowHeight
Expand Down Expand Up @@ -93,10 +110,35 @@ export class UnwrappedInstructionsWithWorkspace extends React.Component {
window.removeEventListener('resize', this.onResize);
}

/**
* Given a prospective delta, determines how much we can actually change the
* height (accounting for min/max) and changes height by that much.
* @param {number} delta
* @returns {number} How much we actually changed
*/
handleHeightResize = delta => {
const {
instructionsHeight: oldHeight,
instructionsMaxHeight: maxHeight,
setInstructionsRenderedHeight: setHeight
} = this.props;

const newHeight = clamp(oldHeight + delta, MIN_HEIGHT, maxHeight);
setHeight(newHeight);

return newHeight - oldHeight;
};

render() {
return (
<span>
<TopInstructions />
{!this.props.isEmbedView && (
<HeightResizer
position={this.props.instructionsHeight}
onResize={this.handleHeightResize}
/>
)}
<CodeWorkspaceContainer
ref={this.setCodeWorkspaceContainerRef}
topMargin={this.props.instructionsHeight}
Expand All @@ -111,13 +153,21 @@ export class UnwrappedInstructionsWithWorkspace extends React.Component {
export default connect(
function propsFromStore(state) {
return {
instructionsHeight: state.instructions.renderedHeight
instructionsHeight: state.instructions.renderedHeight,
instructionsMaxHeight: Math.min(
state.instructions.maxAvailableHeight,
state.instructions.maxNeededHeight
),
isEmbedView: state.pageConstants.isEmbedView
};
},
function propsFromDispatch(dispatch) {
return {
setInstructionsMaxHeightAvailable(maxHeight) {
dispatch(setInstructionsMaxHeightAvailable(maxHeight));
},
setInstructionsRenderedHeight(height) {
dispatch(setInstructionsRenderedHeight(height));
}
};
}
Expand Down
25 changes: 1 addition & 24 deletions apps/src/templates/instructions/TopInstructions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import styleConstants from '../../styleConstants';
import commonStyles from '../../commonStyles';
import Instructions from './Instructions';
import CollapserIcon from './CollapserIcon';
import HeightResizer from './HeightResizer';
import i18n from '@cdo/locale';
import {ViewType} from '@cdo/apps/code-studio/viewAsRedux';
import queryString from 'query-string';
Expand All @@ -34,7 +33,7 @@ import {WIDGET_WIDTH} from '@cdo/apps/applab/constants';
const HEADER_HEIGHT = styleConstants['workspace-headers-height'];
const RESIZER_HEIGHT = styleConstants['resize-bar-width'];

const MIN_HEIGHT = RESIZER_HEIGHT + 60;
export const MIN_HEIGHT = RESIZER_HEIGHT + 60;

const TabType = {
INSTRUCTIONS: 'instructions',
Expand Down Expand Up @@ -325,22 +324,6 @@ class TopInstructions extends Component {
}
};

/**
* Given a prospective delta, determines how much we can actually change the
* height (accounting for min/max) and changes height by that much.
* @param {number} delta
* @returns {number} How much we actually changed
*/
handleHeightResize = delta => {
const currentHeight = this.props.height;

let newHeight = Math.max(MIN_HEIGHT, currentHeight + delta);
newHeight = Math.min(newHeight, this.props.maxHeight);

this.props.setInstructionsRenderedHeight(newHeight);
return newHeight - currentHeight;
};

/**
* Calculate how much height it would take to show top instructions with our
* entire instructions visible and update store with this value.
Expand Down Expand Up @@ -775,12 +758,6 @@ class TopInstructions extends Component {
</div>
)}
</div>
{!this.props.isEmbedView && (
<HeightResizer
position={this.props.height}
onResize={this.handleHeightResize}
/>
)}
</div>
</div>
);
Expand Down
163 changes: 151 additions & 12 deletions apps/test/unit/templates/instructions/InstructionsWithWorkspaceTest.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,38 @@
import $ from 'jquery';
import React from 'react';
import sinon from 'sinon';
import {shallow} from 'enzyme';
import {expect} from '../../../util/reconfiguredChai';
import {UnwrappedInstructionsWithWorkspace as InstructionsWithWorkspace} from '@cdo/apps/templates/instructions/InstructionsWithWorkspace';
import {Provider} from 'react-redux';
import {shallow, mount} from 'enzyme';
import {assert, expect} from '../../../util/reconfiguredChai';
import {
getStore,
registerReducers,
stubRedux,
restoreRedux
} from '@cdo/apps/redux';
import instructionsReducer, {
setInstructionsConstants
} from '@cdo/apps/redux/instructions';
import pageConstantsReducer, {
setPageConstants
} from '@cdo/apps/redux/pageConstants';
import isRtlReducer, {setRtl} from '@cdo/apps/code-studio/isRtlRedux';
import InstructionsWithWorkspace, {
UnwrappedInstructionsWithWorkspace
} from '@cdo/apps/templates/instructions/InstructionsWithWorkspace';

describe('InstructionsWithWorkspace', () => {
const DEFAULT_PROPS = {
instructionsHeight: 400,
instructionsMaxHeight: 400,
isEmbedView: false,
setInstructionsMaxHeightAvailable: () => {},
setInstructionsRenderedHeight: () => {}
};

it('renders instructions and code workspace', () => {
const wrapper = shallow(
<InstructionsWithWorkspace
instructionsHeight={400}
setInstructionsMaxHeightAvailable={() => {}}
/>
<UnwrappedInstructionsWithWorkspace {...DEFAULT_PROPS} />
);

expect(wrapper.find('Connect(TopInstructions)')).to.have.lengthOf(1);
Expand All @@ -20,10 +41,7 @@ describe('InstructionsWithWorkspace', () => {

it('initially does not know window width or height', () => {
const wrapper = shallow(
<InstructionsWithWorkspace
instructionsHeight={400}
setInstructionsMaxHeightAvailable={() => {}}
/>
<UnwrappedInstructionsWithWorkspace {...DEFAULT_PROPS} />
);
expect(wrapper.state()).to.deep.equal({
windowWidth: undefined,
Expand Down Expand Up @@ -51,7 +69,8 @@ describe('InstructionsWithWorkspace', () => {
codeWorkspaceHeight = 100
} = {}) {
const wrapper = shallow(
<InstructionsWithWorkspace
<UnwrappedInstructionsWithWorkspace
{...DEFAULT_PROPS}
instructionsHeight={instructionsHeight}
setInstructionsMaxHeightAvailable={setInstructionsMaxHeightAvailable}
/>
Expand Down Expand Up @@ -135,4 +154,124 @@ describe('InstructionsWithWorkspace', () => {
expect(setInstructionsMaxHeightAvailable).not.to.have.been.called;
});
});

// This is a set of integration tests over the draggable resize grippy's behavior,
// which lives between the instructions area and the code workspace.
// As a result, these tests use much heavier setup than the rest of the file.
describe('resize bar behavior', () => {
beforeEach(() => {
stubRedux();

// Setup minimum redux config for these integration tests
registerReducers({
instructions: instructionsReducer,
isRtl: isRtlReducer,
pageConstants: pageConstantsReducer
});
const store = getStore();
store.dispatch(setRtl(false));
store.dispatch(
setPageConstants({
hideSource: false,
isEmbedView: false,
isShareView: false,
noVisualization: false
})
);
store.dispatch(
setInstructionsConstants({
longInstructions: `Fake instructions`,
noInstructionsWhenCollapsed: true
})
);

// Stub $().outerHeight, which is used to find the height of the instructions content
// in the DOM but doesn't return anything in tests, to always give 500px as the height
// of the instructions content since it gives us something to resize.
sinon.stub($.fn, 'outerHeight').returns(500);
});

afterEach(() => {
$.fn.outerHeight.restore();
restoreRedux();
});

it('can resize instructions by dragging the resize bar', () => {
const store = getStore();
const wrapper = mount(
<Provider store={store}>
<InstructionsWithWorkspace>
<div style={{height: 400}}>
Fake workspace to give the workspace container a height.
</div>
</InstructionsWithWorkspace>
</Provider>
);

const resizer = () => wrapper.find('HeightResizer');
const instructionsHeight = () =>
wrapper
.find('TopInstructions')
.find('.editor-column')
.prop('style').height;

// Initial state
// Instructions content height is stubbed to 500.
// Initial render height is 300.
// Real 'height' style on relevant element is 287 due to 13px resize bar adjustment.
assert.equal(287, instructionsHeight());
assert.include(store.getState().instructions, {
renderedHeight: 300,
expandedHeight: 300,
maxNeededHeight: 543,
maxAvailableHeight: Infinity
});

// Drag the resize bar to make the instructions bigger by 100px
drag(resizer(), 100);
assert.equal(387, instructionsHeight());
assert.include(store.getState().instructions, {
renderedHeight: 400,
expandedHeight: 400,
maxNeededHeight: 543,
maxAvailableHeight: Infinity
});

// Drag the resize bar to make the instructions smaller by 100px
drag(resizer(), -100);
assert.equal(287, instructionsHeight());
assert.include(store.getState().instructions, {
renderedHeight: 300,
expandedHeight: 300,
maxNeededHeight: 543,
maxAvailableHeight: Infinity
});

// Drag the resize bar to make the instructions as large as possible
drag(resizer(), 1000);
assert.equal(530, instructionsHeight());
assert.include(store.getState().instructions, {
renderedHeight: 543,
expandedHeight: 543,
maxNeededHeight: 543,
maxAvailableHeight: Infinity
});

// Drag the resize bar to make the instructions as small as possible
drag(resizer(), -1000);
assert.equal(60, instructionsHeight());
assert.include(store.getState().instructions, {
renderedHeight: 73,
expandedHeight: 73,
maxNeededHeight: 543,
maxAvailableHeight: Infinity
});
});

function drag(element, distance) {
element.simulate('mousedown', {button: 0, pageY: 1000});
element.simulate('mousemove', {pageY: 1000 + distance});
element.simulate('mouseup', {});
}
});
});