-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: replace d2-ui-core controlbar by moving component into dashboa…
…rds repo (#318) Moved the component out of d2-ui-core and simplified it to meet just our needs for dashboards.
- Loading branch information
1 parent
41de2ef
commit 32dd94e
Showing
13 changed files
with
566 additions
and
304 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { colors } from '@dhis2/ui-core'; | ||
import { HEADERBAR_HEIGHT } from './controlBarDimensions'; | ||
|
||
import classes from './styles/ControlBar.module.css'; | ||
|
||
const DRAG_HANDLE_HEIGHT = 7; | ||
|
||
class ControlBar extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
|
||
this.state = { | ||
dragging: false, | ||
}; | ||
} | ||
|
||
onStartDrag = () => { | ||
this.setState({ dragging: true }); | ||
window.addEventListener('mousemove', this.onDrag); | ||
window.addEventListener('mouseup', this.onEndDrag); | ||
}; | ||
|
||
onDrag = event => { | ||
event.preventDefault(); | ||
event.stopPropagation(); | ||
|
||
const newHeight = event.clientY; | ||
|
||
if ( | ||
this.props.onChangeHeight && | ||
newHeight !== this.props.height && | ||
newHeight > 0 | ||
) { | ||
requestAnimationFrame(() => { | ||
this.props.onChangeHeight(newHeight); | ||
}); | ||
} | ||
}; | ||
|
||
onEndDrag = () => { | ||
this.setState({ dragging: false }); | ||
window.removeEventListener('mousemove', this.onDrag); | ||
window.removeEventListener('mouseup', this.onEndDrag); | ||
|
||
if (this.props.onEndDrag) { | ||
this.props.onEndDrag(); | ||
} | ||
}; | ||
|
||
renderDragHandle() { | ||
return typeof this.props.onChangeHeight === 'function' ? ( | ||
<div | ||
className={classes.draghandle} | ||
style={{ height: DRAG_HANDLE_HEIGHT }} | ||
onMouseDown={this.onStartDrag} | ||
/> | ||
) : null; | ||
} | ||
|
||
render() { | ||
const height = Math.max(this.props.height, 0) + DRAG_HANDLE_HEIGHT; | ||
|
||
const rootStyle = Object.assign( | ||
{ | ||
height, | ||
top: HEADERBAR_HEIGHT, | ||
backgroundColor: this.props.editMode | ||
? colors.yellow050 | ||
: colors.white, | ||
paddingBottom: DRAG_HANDLE_HEIGHT, | ||
}, | ||
// Disable animations while dragging | ||
this.state.dragging ? { transition: 'none' } : {} | ||
); | ||
|
||
return ( | ||
<div style={rootStyle} className={classes.root}> | ||
<div className={classes.content}>{this.props.children}</div> | ||
{this.renderDragHandle()} | ||
</div> | ||
); | ||
} | ||
} | ||
|
||
ControlBar.propTypes = { | ||
/** | ||
* The height of the control bar in number of lines. Must be a positive integer. | ||
*/ | ||
height: PropTypes.number.isRequired, | ||
|
||
/** | ||
* If true, the background color of the control bar changes to indicate that edit mode is active. | ||
*/ | ||
editMode: PropTypes.bool.isRequired, | ||
|
||
/** | ||
* Callback function that is called when the control bar is resized. | ||
* The callback receives one argument: The new height in pixels. | ||
* | ||
* If no callback is specified the control bar will not have a drag handle. | ||
*/ | ||
onChangeHeight: PropTypes.func, | ||
|
||
/** | ||
* Callback function that is called when the control bar is dropped after being dragged. | ||
* The callback receives one argument: The new height in pixels. | ||
* | ||
* Ignored if no "onChangeHeight" function is provided. | ||
*/ | ||
onEndDrag: PropTypes.func, | ||
|
||
/** | ||
* The contents of the control bar. | ||
*/ | ||
children: PropTypes.node.isRequired, | ||
}; | ||
|
||
ControlBar.defaultProps = { | ||
onChangeHeight: null, | ||
onEndDrag: null, | ||
}; | ||
|
||
export default ControlBar; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import React from 'react'; | ||
import { shallow } from 'enzyme'; | ||
|
||
import ControlBar from '../ControlBar'; | ||
|
||
jest.mock('@dhis2/ui-core', () => { | ||
return { | ||
colors: { white: 'white', yellow050: 'yellow' }, | ||
}; | ||
}); | ||
|
||
describe('ControlBar', () => { | ||
let props; | ||
const children = ( | ||
<div> | ||
<p>Child</p> | ||
<p>elements</p> | ||
</div> | ||
); | ||
|
||
const renderControlBar = () => | ||
shallow(<ControlBar {...props}>{children}</ControlBar>); | ||
|
||
const requestAnimationFrame = callback => callback(); | ||
|
||
beforeEach(() => { | ||
props = { | ||
height: 32, | ||
editMode: false, | ||
}; | ||
}); | ||
|
||
it('should render a div', () => { | ||
expect(renderControlBar().type()).toBe('div'); | ||
}); | ||
|
||
it('should contain the specified children', () => { | ||
expect(renderControlBar().contains(children)).toBe(true); | ||
}); | ||
|
||
it('should render the drag handle when onChangeHeight callback is specified', () => { | ||
props.onChangeHeight = jest.fn(); | ||
const controlBar = renderControlBar(); | ||
|
||
expect(controlBar.children().length).toBe(2); | ||
expect(controlBar.childAt(1).prop('className')).toBe('draghandle'); | ||
}); | ||
|
||
it('should not render the drag handle when no onChangeHeight callback is specified', () => { | ||
expect(renderControlBar().children().length).toBe(1); | ||
}); | ||
|
||
it('should change the background color in edit mode', () => { | ||
props.editMode = true; | ||
|
||
expect(renderControlBar().props().style.backgroundColor).not.toBe( | ||
'white' | ||
); | ||
}); | ||
|
||
it('should call the onChangeHeight callback when the drag handle is dragged', () => { | ||
// Replace the async DOM function with a sync function that immediately executes the callback | ||
window.requestAnimationFrame = requestAnimationFrame; | ||
|
||
props.onChangeHeight = jest.fn(); | ||
const dragFlap = renderControlBar().childAt(1); | ||
|
||
dragFlap.simulate('mousedown'); | ||
window.dispatchEvent(new MouseEvent('mousemove', { clientY: 1234 })); | ||
window.dispatchEvent(new Event('mouseup')); | ||
|
||
expect(props.onChangeHeight.mock.calls.length).toBe(1); | ||
}); | ||
|
||
it('should only call the onChangeHeight callback if the height actually changed', () => { | ||
// Replace the async DOM function with a sync function that immediately executes the callback | ||
window.requestAnimationFrame = requestAnimationFrame; | ||
|
||
props.onChangeHeight = jest.fn(); | ||
const initialHeight = 1234; | ||
const randomMousePosition = 4321; | ||
props.height = initialHeight; | ||
|
||
const dragFlap = renderControlBar().childAt(1); | ||
|
||
dragFlap.simulate('mousedown'); | ||
window.dispatchEvent( | ||
new MouseEvent('mousemove', { clientY: randomMousePosition }) | ||
); | ||
expect(props.onChangeHeight.mock.calls.length).toBe(1); | ||
const newHeight = initialHeight - props.onChangeHeight.mock.calls[0][0]; | ||
|
||
window.dispatchEvent( | ||
new MouseEvent('mousemove', { | ||
clientY: newHeight + randomMousePosition, | ||
}) | ||
); | ||
window.dispatchEvent(new Event('mouseup')); | ||
|
||
expect(props.onChangeHeight.mock.calls.length).toBe(1); | ||
}); | ||
|
||
it('should disable CSS transitions while dragging', () => { | ||
props.onChangeHeight = jest.fn(); | ||
const controlBar = renderControlBar(); | ||
const dragFlap = controlBar.childAt(1); | ||
|
||
expect(controlBar.props().style.transition).not.toBe('none'); | ||
dragFlap.simulate('mousedown'); | ||
expect(controlBar.props().style.transition).toBe('none'); | ||
|
||
window.dispatchEvent(new MouseEvent('mouseup')); | ||
controlBar.update(); | ||
expect(controlBar.props().style.transition).not.toBe('none'); | ||
}); | ||
|
||
it('should start listening for mousemove and mouseup events when the drag flap is clicked', () => { | ||
const spy = jest.spyOn(window, 'addEventListener'); | ||
|
||
props.onChangeHeight = jest.fn(); | ||
const dragFlap = renderControlBar().childAt(1); | ||
|
||
dragFlap.last().simulate('mousedown'); | ||
const eventHandlers = window.addEventListener.mock.calls.map( | ||
args => args[0] | ||
); | ||
expect(eventHandlers.includes('mousemove')).toBe(true); | ||
expect(eventHandlers.includes('mouseup')).toBe(true); | ||
|
||
spy.mockReset(); | ||
spy.mockRestore(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.