This is part of the Ext JS to React blog series. You can review the code from this article on the Ext JS to React Git repo.
Components are commonly rendered to the page in a rather stationary layout. But, sometimes you want to position a component above the others. Modal alerts, loading indicators, context menus, and tooltips all want to be positioned absolutely on the page. With React we’ll use CSS to position our floating components right where we want them.
Note: While not a requirement for React, this article’s code examples assume you’re starting with a starter app generated by create-react-app.
NEW RESEARCH: LEARN HOW DECISION-MAKERS ARE PRIORITIZING DIGITAL INITIATIVES IN 2024.
React Menu Class
A common use case is to allow users to right-click to view a menu of options relating to wherever they just clicked. To demonstrate how we can do this in React we’ll modify the starter app generated by create-react-app. Let’s say that we want a popup menu to appear when we right-click in the target div
and that we want the menu to appear right where we clicked.
First, let’s define our menu component. Our menu will accept three props: show
, x
, and y
. The show
prop determines whether the menu will be visible or not. The x
and y
props will set the position of the menu on the page.
import React, { Component } from 'react'; import './Menu.css'; class Menu extends Component { state = {} showMenu = (show) => { this.setState({ show }); } componentWillReceiveProps ({ show }) { this.showMenu(show); } componentDidMount () { document.body.addEventListener('click', this.showMenu, false); } componentWillUnmount () { document.body.removeEventListener('click', this.showMenu, false); } render() { const { x, y, show : menuShow } = this.props; const { show = menuShow } = this.state; const menuStyle = { top: y, left: x }; return show ? ( <div style={menuStyle} className="menu"> {this.props.children} </div> ) : null; } } export default Menu;
React Menu Class Explained
The Menu
class employs the following methods:
- showMenu: sets the value of show on the component state. This value is what determines visibility within the render method.
- componentWillReceiveProps: The
show
value of the component state is updated when new props are passed in - componentDidMount: We want the menu to hide when a click event is registered. Here we add a click listener to the document body to hide the menu.
- componentWillUnmount: When the menu component is unmounted, we remove the document body click handler
- render: The menu element itself is relatively simple as it’s just a
div
with a CSS class name of “menu” and is absolutely positioned using the component’s props and state. The menudiv
wraps any children passed in.
React Menu CSS
Below is the contents of the CSS file imported by the Menu class:
.menu { background: white; position: absolute; } .menu > hr { margin: 5px 0; border: 1px solid #ececec; } .menu-item { color: #555; padding: 12px; cursor: pointer; font-size: 16px; } .menu-item:hover { background: #c7c7c7; }
To use the menu in our target div
we’ll add an onContextMenu
handler that sets the menuShow
, x
, and y
values on the state of our App class. That state value will then be passed down to the Menu as a show
prop:
import React, { Component } from 'react'; import Menu from './Menu'; const style = { height: '300px', width: '300px', background: '#e02d42', padding: '12px', color: 'white', textAlign: 'center' } class App extends Component { state = {} handleContextMenu = (e) => { e.preventDefault(); const { clientX : x, clientY : y } = e; const menuShow = true; this.setState({ menuShow, x, y }); } render() { const { menuShow, x, y } = this.state; return ( <div style={style} onContextMenu={this.handleContextMenu}> right-click in this div to show a menu <Menu show={menuShow} x={x} y={y}> <div className="menu-item">First Menu Item</div> <hr /> <div className="menu-item">Second Menu Item</div> </Menu> </div> ); } } export default App;
You can review the sample code on the Git repo.
React Tooltips Class
Let’s consider how we might go about displaying tooltips in our apps. We’ll want to display some text when hovering the cursor over a target element / component on the page. To do that, we will wrap any target element with a div
used to display a tooltip. The Tooltip
will accept two props: tiptext
which we use as the text of the tooltip and position
which will be a class name used to position the tooltip next to our target element:
import React from 'react'; import './Tooltip.css'; const Tooltip = ({ children, tiptext, position = 't-t'}) => ( <div data-tiptext={tiptext} className={`tooltip ${position}`}> {children} </div> ) export default Tooltip;
React Tooltip CSS
In this case, the CSS needed is much weightier than the Tooltip
class itself.
.tooltip { position: relative; display: inline-block; white-space: nowrap; } .tooltip:after { content: attr(data-tiptext); opacity: 0; position: absolute; padding: 6px 12px; background: #555; color: white; font-size: 12px; border-radius: 4px; pointer-events: none; transition-property: opacity, transform; transition-duration: .2s; } .tooltip:hover:after { opacity : 1; transition-delay: .3s; } .t-t:after { left: 50%; transform: translate(-50%, calc(-100% - 6px)); top: 0; } .t-t:hover:after { transform: translate(-50%, calc(-100% - 12px)); } .b-b:after { left: 50%; bottom: 0; transform: translate(-50%, calc(100% + 6px)); } .b-b:hover:after { transform: translate(-50%, calc(100% + 12px)); }
To demonstrate, we’ll render some text and wrap the word “moon” with our Tooltip
.
import React, { Component } from 'react'; import Tooltip from './Tooltip'; class App extends Component { render() { return ( <div style={{fontSize: '20px', color: '#555', margin: '60px'}}> <span>THAT'S NO </span> <Tooltip tiptext="DEATH STAR"> <span style={{color: '#a0a0a0'}}>MOON</span> </Tooltip> </div> ); } } export default App;
Hovering the cursor over the word “MOON” will show the tooltip above it. The position of 't-t'
used to position the tooltip over our target is the default value so we can leave it out of the Tooltip
instance.
If you’d prefer an out-of-the-box solution you might look at Tippy.js. Tippy.js is a javascript solution built on Popper.js that lets you create tooltips that show on hover, show on click, show on delay, contain HTML, are closable, have animations built in, and more!
You can review the sample code on the Git repo.
To Modal or not to Modal
The final popular use case we’ll look at is floating modal components – what you might think of in Ext JS as a MsgBox
or a Window
. For this example we’ll keep the contents of the floating component generic. That allows you to pass in any element or component you choose and have it float above the application. We’ll also make the modal mask an optional property so that you can choose whether you want to allow users to interact with the underlying application components.
Before we get to our floating component code, let’s take just a moment to discuss a new feature from React 16 that we’ll be taking advantage of: portals. Portals allow a component to be rendered anywhere in the DOM no matter where the component is placed in the overall component composition. If we want a floating loading message to appear in the containing element in which our floating component lives, then there’s no need to use ReactDOM.createPortal()
in the render method. However, if a given component’s load action should mask the entire viewport, then that’s where the portal feature will come in handy. To see it in action, let’s first take a look at the Floating
component class.
import React, { Component } from 'react'; // Required to use React portals import ReactDOM from 'react-dom'; import './Floating.css'; class Floating extends Component { state = { show: !!this.props.show }; componentWillReceiveProps ({ show }) { this.setState({ show }); } handleModalClick = () => { this.setState({ show: false }); }; render () { const { modal, viewport, closeOnModalClick } = this.props; const { show } = this.state; const isModal = modal ? ' modal' : ''; const doShow = show ? ' show' : ''; const isViewport = viewport ? ' viewport' : ''; const clsName = `float-wrap${isModal}${doShow}${isViewport}`; const props = { onClick: closeOnModalClick ? this.handleModalClick : null }; const float = ( <div {...props} className={clsName}> {this.props.children} </div> ); return viewport ? ReactDOM.createPortal(float, document.body) : float; } } export default Floating;
React Floating Class Explained
Let’s look at the methods from our Floating
component:
- constructor: In the
constructor
method, we set theshow
property of the component state using theshow
prop. Theshow
value will be used to display our floating component via a CSS class name. - onModalClick: Handles setting the show value on the component state to false to hide the floating component. Will only be called if the
closeOnModalClick
property is passed. - render: Defines the basic structure of our floating component. The return value is what deserves special mention. If the
viewport
property is passed, we’ll render our component as a portal usingReactDOM.createPortal
. The portal will render to the document body and mask our entire application. Ifviewport
is not passed, then our floating component will render into the DOM naturally as it’s composed.
React Floating Class CSS
Our floating component only has a few methods since most of the modal and positioning business is accomplished via CSS.
.float-wrap { position: absolute; top: 0; bottom: 0; left: 0; right: 0; display: flex; align-items: center; justify-content: center; pointer-events: none; display: none; } .modal { background-color: rgba(0, 0, 0, .1); pointer-events: auto; } .modal-msg { padding: 12px 24px 12px 0; background: #ffffff; border: 3px solid #61dafb; color: #555; } .viewport { position: fixed; } .modal-msg > * { vertical-align: middle; } .show { display: flex; }
React Floating Component Example
Let’s see our Floating
component in action with a simple loading message as the body of the floating component. The Floating
component will initially be hidden and shown when clicking on the “Show Floating” button
. Clicking on the modal mask will hide the Floating
component. Then, to render the Floating component to the wrapping div
all we would need to do is omit the viewport
prop. Below shows how to render the Floating component across the entire viewport / application:
import React, { Component } from 'react'; import logo from './logo.svg'; import Floating from './Floating'; import './App.css'; class App extends Component { state = {}; showFloating = () => { this.setState({ floating: true }); }; render() { const { floating } = this.state; return ( <div style={{width: '400px', height: '400px', position: 'relative'}}> <button onClick={this.showFloating}>Show Floating</button> <Floating modal show={floating} viewport closeOnModalClick> <div className="modal-msg"> <img src={logo} className="App-logo" alt="logo" /> Loading... </div> </Floating> </div> ); } } export default App;
We’ve composed the Floating
component in a div
just below our “show” button
in this example, but it could have been composed anywhere since we’re rendering it directly to the document body. React did gift us with a bit of magic with portals, though. If the events fired on the portal should bubble up to the components above it in the JSX hierarchy, then composition of our Floating
component would matter. See the React guide for more information.
You can review the sample code on the Git repo.
Conclusion
These small examples demonstrate what you can do with just a little React and CSS for the most elemental use cases. If your application grows in complexity you may need to implement a floating component manager that sets z-index automatically. Other libraries such as react-boostrap, material-ui, and semantic-ui-react also provide many of the floating components you may need if you’re looking for additional pre-built solutions. Your menu component may need to accept a sub-menus component: <MenuItem><Menu /></MenuItem>
. With React applications consisting of small, reusable components, each component can be imported as needed so that your application imports complexity only when it needs it!
Seth Lemmons
Related Posts
-
Ext JS to React: Migration to Open Source
Worried about Migrating from Ext JS? Modus has the Answers Idera’s acquisition of Sencha has…
-
Ext JS to React: FAQ
This is part of the Ext JS to React blog series. React is Facebook's breakout…