Center tooltip along top or bottom of target

This adjusts the positioning to work more the way we want:

* Tooltip is position on the top or bottom edge of the target depending on where
  space is available
* Tooltip and chevron are centered

In addition, more bits borrowed from `ContextualMenu` are not needed, so they
have been removed for simplicity.

Part of https://github.com/vector-im/riot-web/issues/9753
Part of https://github.com/vector-im/riot-web/issues/9716
This commit is contained in:
J. Ryan Stinnett 2019-06-20 18:33:45 +01:00
parent 6dcdad028e
commit 32bf4588dd
2 changed files with 50 additions and 116 deletions

View file

@ -39,79 +39,13 @@ limitations under the License.
z-index: 5001; z-index: 5001;
} }
.mx_InteractiveTooltip_right {
right: 0;
}
.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_right {
right: 8px;
}
.mx_InteractiveTooltip_chevron_right {
position: absolute;
right: -8px;
top: 0px;
width: 0;
height: 0;
border-top: 8px solid transparent;
border-left: 8px solid $menu-bg-color;
border-bottom: 8px solid transparent;
}
.mx_InteractiveTooltip_chevron_right::after {
content: '';
width: 0;
height: 0;
border-top: 7px solid transparent;
border-left: 7px solid $menu-bg-color;
border-bottom: 7px solid transparent;
position: absolute;
top: -7px;
right: 1px;
}
.mx_InteractiveTooltip_left {
left: 0;
}
.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_left {
left: 8px;
}
.mx_InteractiveTooltip_chevron_left {
position: absolute;
left: -8px;
top: 0px;
width: 0;
height: 0;
border-top: 8px solid transparent;
border-right: 8px solid $menu-bg-color;
border-bottom: 8px solid transparent;
}
.mx_InteractiveTooltip_chevron_left::after {
content: '';
width: 0;
height: 0;
border-top: 7px solid transparent;
border-right: 7px solid $menu-bg-color;
border-bottom: 7px solid transparent;
position: absolute;
top: -7px;
left: 1px;
}
.mx_InteractiveTooltip_top {
top: 0;
}
.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_top { .mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_top {
top: 8px; top: 8px;
} }
.mx_InteractiveTooltip_chevron_top { .mx_InteractiveTooltip_chevron_top {
position: absolute; position: absolute;
left: 0px; left: calc(50% - 8px);
top: -8px; top: -8px;
width: 0; width: 0;
height: 0; height: 0;
@ -132,17 +66,13 @@ limitations under the License.
top: 1px; top: 1px;
} }
.mx_InteractiveTooltip_bottom {
bottom: 0;
}
.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_bottom { .mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_bottom {
bottom: 8px; bottom: 8px;
} }
.mx_InteractiveTooltip_chevron_bottom { .mx_InteractiveTooltip_chevron_bottom {
position: absolute; position: absolute;
left: 0px; left: calc(50% - 8px);
bottom: -8px; bottom: -8px;
width: 0; width: 0;
height: 0; height: 0;

View file

@ -33,21 +33,19 @@ function getOrCreateContainer() {
return container; return container;
} }
/*
* This style of tooltip takes a `target` element's rect and centers the tooltip
* along one edge of the target.
*/
export default class InteractiveTooltip extends React.Component { export default class InteractiveTooltip extends React.Component {
propTypes: { propTypes: {
top: PropTypes.number, // A DOMRect from the target element
bottom: PropTypes.number, targetRect: PropTypes.object.isRequired,
left: PropTypes.number,
right: PropTypes.number,
chevronOffset: PropTypes.number,
chevronFace: PropTypes.string, // top, bottom, left, right or none
// Function to be called on menu close // Function to be called on menu close
onFinished: PropTypes.func, onFinished: PropTypes.func,
// If true, insert an invisible screen-sized element behind the // If true, insert an invisible screen-sized element behind the
// menu that when clicked will close it. // menu that when clicked will close it.
hasBackground: PropTypes.bool, hasBackground: PropTypes.bool,
// The component to render as the context menu // The component to render as the context menu
elementClass: PropTypes.element.isRequired, elementClass: PropTypes.element.isRequired,
// on resize callback // on resize callback
@ -56,58 +54,64 @@ export default class InteractiveTooltip extends React.Component {
closeTooltip: PropTypes.func, closeTooltip: PropTypes.func,
}; };
constructor() {
super();
this.state = {
contentRect: null,
};
}
collectContentRect = (element) => {
// We don't need to clean up when unmounting, so ignore
if (!element) return;
this.setState({
contentRect: element.getBoundingClientRect(),
});
}
render() { render() {
const props = this.props;
const { targetRect } = props;
// The window X and Y offsets are to adjust position when zoomed in to page
const targetLeft = targetRect.left + window.pageXOffset;
const targetBottom = targetRect.bottom + window.pageYOffset;
const targetTop = targetRect.top + window.pageYOffset;
// Align the tooltip vertically on whichever side of the target has more
// space available.
const position = {}; const position = {};
let chevronFace = null; let chevronFace = null;
const props = this.props; if (targetBottom < window.innerHeight / 2) {
position.top = targetBottom;
if (props.top) { chevronFace = "top";
position.top = props.top;
} else { } else {
position.bottom = props.bottom; position.bottom = window.innerHeight - targetTop;
chevronFace = "bottom";
} }
if (props.left) { // Center the tooltip horizontally with the target's center.
position.left = props.left; position.left = targetLeft + targetRect.width / 2;
chevronFace = 'left';
} else {
position.right = props.right;
chevronFace = 'right';
}
const chevronOffset = {}; const chevron = <div className={"mx_InteractiveTooltip_chevron_" + chevronFace} />;
if (props.chevronFace) {
chevronFace = props.chevronFace;
}
const hasChevron = chevronFace && chevronFace !== "none";
if (chevronFace === 'top' || chevronFace === 'bottom') {
chevronOffset.left = props.chevronOffset;
} else {
chevronOffset.top = props.chevronOffset;
}
const chevron = hasChevron ?
<div style={chevronOffset} className={"mx_InteractiveTooltip_chevron_" + chevronFace} /> :
undefined;
const className = 'mx_InteractiveTooltip_wrapper';
const menuClasses = classNames({ const menuClasses = classNames({
'mx_InteractiveTooltip': true, 'mx_InteractiveTooltip': true,
'mx_InteractiveTooltip_left': !hasChevron && position.left,
'mx_InteractiveTooltip_right': !hasChevron && position.right,
'mx_InteractiveTooltip_top': !hasChevron && position.top,
'mx_InteractiveTooltip_bottom': !hasChevron && position.bottom,
'mx_InteractiveTooltip_withChevron_left': chevronFace === 'left',
'mx_InteractiveTooltip_withChevron_right': chevronFace === 'right',
'mx_InteractiveTooltip_withChevron_top': chevronFace === 'top', 'mx_InteractiveTooltip_withChevron_top': chevronFace === 'top',
'mx_InteractiveTooltip_withChevron_bottom': chevronFace === 'bottom', 'mx_InteractiveTooltip_withChevron_bottom': chevronFace === 'bottom',
}); });
const menuStyle = {};
if (this.state.contentRect) {
menuStyle.left = `-${this.state.contentRect.width / 2}px`;
}
const ElementClass = props.elementClass; const ElementClass = props.elementClass;
return <div className={className} style={{...position}}> return <div className="mx_InteractiveTooltip_wrapper" style={{...position}}>
<div className={menuClasses}> <div className={menuClasses} style={menuStyle} ref={this.collectContentRect}>
{ chevron } { chevron }
<ElementClass {...props} onFinished={props.closeTooltip} onResize={props.windowResize} /> <ElementClass {...props} onFinished={props.closeTooltip} onResize={props.windowResize} />
</div> </div>