diff --git a/package.json b/package.json index 612806f8..f4db1f3f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-timelines", - "version": "2.4.0", + "version": "2.5.0", "description": "React Timelines", "main": "lib/index.js", "scripts": { @@ -42,7 +42,7 @@ "babel-preset-react": "^6.23.0", "babel-preset-stage-2": "^6.22.0", "enzyme": "^3.3.0", - "enzyme-adapter-react-16": "^1.1.1", + "enzyme-adapter-react-16": "npm:enzyme-react-adapter-future", "eslint": "^4.19.1", "eslint-config-airbnb": "^16.1.0", "eslint-plugin-import": "^2.12.0", diff --git a/src/components/Contexts/Viewport.js b/src/components/Contexts/Viewport.js new file mode 100644 index 00000000..84086f52 --- /dev/null +++ b/src/components/Contexts/Viewport.js @@ -0,0 +1,6 @@ +import React from 'react' + +export default React.createContext({ + left: 0, + right: 0 +}) diff --git a/src/components/Elements/Basic.jsx b/src/components/Elements/Basic.jsx index d11acca6..300dc5dc 100644 --- a/src/components/Elements/Basic.jsx +++ b/src/components/Elements/Basic.jsx @@ -1,7 +1,9 @@ import React from 'react' import PropTypes from 'prop-types' -import { getDayMonth } from '../../utils/formatDate' + import createClasses from '../../utils/classes' +import Tooltip from './Tooltip' +import Viewport from '../Contexts/Viewport' const buildDataAttributes = (attributes = {}) => { const value = {} @@ -22,20 +24,19 @@ const Basic = ({ -
+ { - tooltip - // eslint-disable-next-line react/no-danger - ?
') }} /> - : ( -
-
{title}
-
Start {getDayMonth(start)}
-
End {getDayMonth(end)}
-
+ viewport => ( + ) } -
+
) diff --git a/src/components/Elements/Tooltip.jsx b/src/components/Elements/Tooltip.jsx new file mode 100644 index 00000000..8b04a1be --- /dev/null +++ b/src/components/Elements/Tooltip.jsx @@ -0,0 +1,85 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' + +import { getDayMonth } from '../../utils/formatDate' + +class Tooltip extends Component { + constructor(props) { + super(props) + + this.selfRef = React.createRef() + this.state = { + bounds: {} + } + } + + componentDidMount() { + const bounds = this.selfRef.current.getBoundingClientRect() + + // eslint-disable-next-line react/no-did-mount-set-state + this.setState({ bounds }) + } + + render() { + const { + title, + start, + end, + tooltip, + viewport + } = this.props + const { bounds } = this.state + + let offset = 0 + let className = 'rt-element__tooltip' + + if (bounds.left < viewport.left) { + offset = Math.floor(viewport.left - bounds.left) + className += ' rt-element__tooltip--left' + } + + if (bounds.right > viewport.right) { + offset = -Math.ceil(bounds.right - viewport.right) + className += ' rt-element__tooltip--right' + } + + const style = { + marginLeft: `${offset}px` + } + + return ( +
+ { + tooltip + // eslint-disable-next-line react/no-danger + ?
') }} /> + : ( +
+
{title}
+
Start {getDayMonth(start)}
+
End {getDayMonth(end)}
+
+ ) + } +
+ ) + } +} + +Tooltip.propTypes = { + title: PropTypes.string.isRequired, + start: PropTypes.instanceOf(Date).isRequired, + end: PropTypes.instanceOf(Date).isRequired, + tooltip: PropTypes.string, + viewport: PropTypes.shape({ + left: PropTypes.number + }) +} + +Tooltip.defaultProps = { + viewport: { + left: 0 + } +} + +export default Tooltip diff --git a/src/components/Elements/__tests__/Basic.jsx b/src/components/Elements/__tests__/Basic.jsx index 84cc0eef..6c015802 100644 --- a/src/components/Elements/__tests__/Basic.jsx +++ b/src/components/Elements/__tests__/Basic.jsx @@ -1,5 +1,5 @@ import React from 'react' -import { shallow } from 'enzyme' +import { mount, shallow } from 'enzyme' import Basic from '../Basic' @@ -19,14 +19,14 @@ describe('', () => { it('renders the tooltip value if it exists', () => { const tooltip = 'Test tooltip' const props = { ...defaultProps, tooltip } - const wrapper = shallow() + const wrapper = mount() expect(getTooltip(wrapper).html()).toMatch('Test tooltip') }) it('handles multiline tooltips', () => { const tooltip = 'Test\ntooltip' const props = { ...defaultProps, tooltip } - const wrapper = shallow() + const wrapper = mount() expect(getTooltip(wrapper).html()).toMatch('Test
tooltip') }) @@ -38,7 +38,7 @@ describe('', () => { const props = { ...defaultProps, tooltip, title, start, end } - const wrapper = shallow() + const wrapper = mount() expect(getTooltip(wrapper).text()).toMatch('TEST') expect(getTooltip(wrapper).text()).toMatch('Start 20 Mar') expect(getTooltip(wrapper).text()).toMatch('End 15 Apr') @@ -46,7 +46,7 @@ describe('', () => { it('can take an optional list of classnames to add to the parent', () => { const props = { ...defaultProps, classes: ['foo', 'bar'] } - const wrapper = shallow() + const wrapper = mount() expect(wrapper.find('.rt-element').hasClass('foo')).toBe(true) expect(wrapper.find('.rt-element').hasClass('bar')).toBe(true) }) diff --git a/src/components/Layout/index.jsx b/src/components/Layout/index.jsx index 9fccd79e..2d25844c 100644 --- a/src/components/Layout/index.jsx +++ b/src/components/Layout/index.jsx @@ -1,6 +1,7 @@ -import React, { PureComponent } from 'react' +import React, { Component } from 'react' import PropTypes from 'prop-types' +import ViewportContext from '../Contexts/Viewport' import Sidebar from '../Sidebar' import Timeline from '../Timeline' import { addListener, removeListener } from '../../utils/events' @@ -9,7 +10,7 @@ import getNumericPropertyValue from '../../utils/getNumericPropertyValue' const noop = () => {} -class Layout extends PureComponent { +class Layout extends Component { constructor(props) { super(props) @@ -20,7 +21,8 @@ class Layout extends PureComponent { this.state = { isSticky: false, headerHeight: 0, - scrollLeft: 0 + scrollLeft: 0, + viewport: {} } } @@ -36,14 +38,13 @@ class Layout extends PureComponent { } componentDidUpdate(prevProps, prevState) { - if (this.props.enableSticky && this.state.isSticky) { - if (!prevState.isSticky) { - this.updateTimelineHeaderScroll() - } + if (this.props.enableSticky && this.state.isSticky && !prevState.isSticky) { + this.updateTimelineHeaderScroll() + } - if (this.state.scrollLeft !== prevState.scrollLeft) { - this.updateTimelineBodyScroll() - } + if (this.state.scrollLeft !== prevState.scrollLeft) { + this.updateTimelineBodyScroll() + this.updateTimelineViewport() } if (this.props.isOpen !== prevProps.isOpen) { @@ -64,9 +65,12 @@ class Layout extends PureComponent { scrollToNow = () => { const { time, scrollToNow, now } = this.props + if (scrollToNow) { this.timeline.current.scrollLeft = time.toX(now) - (0.5 * this.props.timelineViewportWidth) } + + this.updateTimelineHeaderScroll() } updateTimelineBodyScroll = () => { @@ -75,7 +79,17 @@ class Layout extends PureComponent { updateTimelineHeaderScroll = () => { const { scrollLeft } = this.timeline.current - this.setState({ scrollLeft }) + this.setState({ scrollLeft }, () => this.updateTimelineViewport()) + } + + updateTimelineViewport = () => { + const { left, right } = this.timeline.current.getBoundingClientRect() + this.setState({ + viewport: { + left: left + this.state.scrollLeft, + right: right + this.state.scrollLeft + } + }) } handleHeaderScrollY = (scrollLeft) => { @@ -136,8 +150,10 @@ class Layout extends PureComponent { const { isSticky, headerHeight, - scrollLeft + scrollLeft, + viewport } = this.state + return (
- + + +
diff --git a/src/scss/components/_element.scss b/src/scss/components/_element.scss index dd20b926..8dd1450c 100644 --- a/src/scss/components/_element.scss +++ b/src/scss/components/_element.scss @@ -17,6 +17,8 @@ text-overflow: ellipsis; } +$tooltip-size: 6px; + .rt-element__tooltip { position: absolute; bottom: 100%; @@ -28,25 +30,35 @@ text-align: left; background: $react-timelines-text-color; color: white; - transform: translateX(-50%) scale(0); + transform: translateX(-50%); + opacity: 0; pointer-events: none; &::before { - $size: 6px; + $tooltip-size: 6px; position: absolute; top: 100%; left: 50%; - border-top: $size solid $react-timelines-text-color; - border-right: $size solid transparent; - border-left: $size solid transparent; + border-top: $tooltip-size solid $react-timelines-text-color; + border-right: $tooltip-size solid transparent; + border-left: $tooltip-size solid transparent; transform: translateX(-50%); content: ' '; } } +.rt-element__tooltip--left::before { + left: $tooltip-size; +} + +.rt-element__tooltip--right::before { + left: 100%; + margin-left: -$tooltip-size; +} + .rt-element:hover > .rt-element__tooltip, .rt-element:focus > .rt-element__tooltip { $delay: 0.3s; - transform: translateX(-50%) scale(1); - transition: transform 0s $delay; + opacity: 1; + transition: opacity 0s $delay; } diff --git a/yarn.lock b/yarn.lock index 2e439a26..9425b781 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1753,13 +1753,13 @@ entities@^1.1.1, entities@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" -enzyme-adapter-react-16@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.1.1.tgz#a8f4278b47e082fbca14f5bfb1ee50ee650717b4" +"enzyme-adapter-react-16@npm:enzyme-react-adapter-future": + version "1.1.3" + resolved "https://registry.yarnpkg.com/enzyme-react-adapter-future/-/enzyme-react-adapter-future-1.1.3.tgz#f0c102f098086a0ad0270bbdaf9da5113297dc05" dependencies: enzyme-adapter-utils "^1.3.0" lodash "^4.17.4" - object.assign "^4.0.4" + object.assign "^4.1.0" object.values "^1.0.4" prop-types "^15.6.0" react-reconciler "^0.7.0"