Popovers can pop out in many different directions. We’re going to make popovers in each of these directions for practice:
- Top
- Left
- Right
- Bottom
We’ll make the left popover first.
Making the left popover
First, we need to add the trigger and popover into the HTML:
<!-- Popover trigger --><button class="popover-trigger" data-popover-position="left"> <svg viewBox="0 0 40 20"> <use xlink:href="#arrow"></use> </svg></button><!-- Popover HTML --><div class="popover" data-position="left"> <p>The quick brown fox jumps over the lazy dog.</p></div>Code for the left popover is similar to the top popover. It can be hard to write the left popover’s code if we need to avoid variable name collisions with the top popover. The best way to overcome this is to isolate the top popover’s code.
We can isolate the top popover’s code with a block scope.
{ // Top popover code}Next, we need to select the left popover’s trigger. We can’t just look for .popover-trigger anymore. We need a more specific selector.
Since the left popover is the second popover, one thing we can do is use querySelectorAll.
const popoverTrigger = document.querySelectorAll('.popover-trigger')[1]Then, we need to select the second popover.
const popover = document.querySelectorAll('.popover')[1]To get the popover’s left value, we need to know the trigger’s left value.
const popoverTriggerRect = popoverTrigger.getBoundingClientRect()const triggerLeft = popoverTriggerRect.leftThe trigger’s left value is a sum of the popover’s left, the popover’s width, and some breathing space.
const space = 20const popoverRect = popover.getBoundingClientRect()const leftPosition = triggerLeft - popoverRect.width - spaceThen we set the left position with style.
popover.style.left = `${leftPosition}px`
Next, we need to calculate the left popover’s top position.
To calculate the top position, we need to know the center of the trigger.
const triggerCenter = (popoverTriggerRect.top + popoverTriggerRect.bottom) / 2We know the center of the popover is sum of the the popover’s top position and half of its height.
const topPosition = triggerCenter - popoverRect.height / 2Then we’ll set the top position
popover.style.top = `${topPosition}px`
We need to hide the popover after positioning it.
// Hides popover once it is positionedpopover.setAttribute('hidden', true)We also have to add event listeners to the left popover and its trigger.
// Allows users to show/hide the popoverpopoverTrigger.addEventListener('click', _ => { if (popover.hasAttribute('hidden')) { popover.removeAttribute('hidden') } else { popover.setAttribute('hidden', true) }})
// Hides popover if user clicks outside of the trigger and the popoverdocument.addEventListener('click', event => { if ( event.target.closest('.popover') || event.target.closest('.popover-trigger') ) return popover.setAttribute('hidden', true)})Refactor
Code for the top and left popover are similar. It makes sense to refactor at this point, especially since we still need right and bottom popovers.
First, we need to be able to select all four triggers individually. We can do this with a forEach loop.
const popoverTriggers = document.querySelectorAll('.popover-trigger')
popoverTriggers.forEach(popoverTrigger => { // ...})Next, we need to find the popover the trigger links to. The best way to do this is give an id to each popover.
We can add a custom attribute to the trigger that points to this id. We’ll call this attribute data-target.
<!-- Popovers --><div id="pop-1" class="popover" data-position="top"> <p>The quick brown fox jumps over the lazy dog.</p></div>
<div id="pop-2" class="popover" data-position="left"> <p>The quick brown fox jumps over the lazy dog.</p></div><!-- Triggers --><button class="popover-trigger" data-trigger="pop-1" ...>...</button><button class="popover-trigger" data-trigger="pop-2" ...>...</button>We can find the popover with the id attribute.
popoverTriggers.forEach(popoverTrigger => { const popover = document.querySelector(`#${popoverTrigger.dataset.target}`) // ...})Next, we need to calculate each popover’s top and left position. We can leave the calculation to a dedicated function. We’ll call this function calculatePopoverPosition.
function calculatePopoverPosition() { // ...}We need to know three things to calculate the popover’s top and left position:
- The trigger’s bounding rectangle
- The popover’s bounding rectangle
- The amount of breathing space
We can get (1) and (2) by passing the trigger and popover into the function. We can get (3) by declaring it directly in calculatePopoverPosition.
function calculatePopoverPosition() { const popoverTriggerRect = popoverTrigger.getBoundingClientRect() const popoverRect = popover.getBoundingClientRect()}One function should only do one thing. This means calculatePopoverPosition should only calculate the top and left values for each popover. It should not set them.
For this to work, we’ll return an object that contains the top and left values.
function calculatePopoverPosition(popoverTrigger, popover) { // ... return { top: 'some-value', left: 'some-value', }}We need to know where to position the popover (top, right, bottom, or left). We can get this information from the data-position custom
function calculatePopoverPosition(popoverTrigger, popover) { // ... const { position } = popover.dataset
return { top: 'some-value', left: 'some-value', }}If position is top, we’ll use the top popover’s calculation.
function calculatePopoverPosition(popoverTrigger, popover) { // ... if (position === 'top') { return { top: popoverTriggerRect.top - popoverRect.height - space, left: (popoverTriggerRect.left + popoverTriggerRect.right) / 2 - popoverRect.width / 2, } } // ...}If the position is left, we’ll use the left popover’s calculation.
function calculatePopoverPosition(popoverTrigger, popover) { // ... if (position === 'left') { return { left: popoverTriggerRect.left - popoverRect.width - space, top: (popoverTriggerRect.top + popoverTriggerRect.bottom) / 2 - popoverRect.height / 2, } } // ...}Next, we will set the popover’s top and left values.
popoverTriggers.forEach(popoverTrigger => { // ... const popoverPosition = calculatePopoverPosition(popoverTrigger, popover)
popover.style.top = `${popoverPosition.top}px` popover.style.left = `${popoverPosition.left}px`})
Then we will hide the popover.
popoverTriggers.forEach(popoverTrigger => { // ... popover.setAttribute('hidden', true)})The event listeners
We wrote two event listeners. Here’s one of them:
popoverTrigger.addEventListener('click', _ => { if (popover.hasAttribute('hidden')) { popover.removeAttribute('hidden') } else { popover.setAttribute('hidden', true) }})We can continue to write one event listener for each trigger, but that’s inefficient. We can use event delegation instead.
To use event delegation, we need add an event listener to the common ancestor of all triggers. Since triggers can be placed anywhere, the common ancestor is the document.
document.addEventListener('click', event => { // ...})If a trigger got clicked, we need to find that trigger.
document.addEventListener('click', event => { const popoverTrigger = event.target.closest('.popover-trigger') if (!popoverTrigger) return})Next, we need to check whether the corresponding popover is shown or hidden. We can tell by checking the hidden attribute.
- If there’s a
hiddenattribute, we know popover is hidden. We show it by removing thehiddenattribute. - If there’s no
hiddenattribute, we know the popover is shown. We hide it by adding thehiddenattribute.
document.addEventListener('click', event => { // ... const popover = document.querySelector(`#${popoverTrigger.dataset.target}`) if (popover.hasAttribute('hidden')) { popover.removeAttribute('hidden') } else { popover.setAttribute('hidden', true) }})
Here’s the second event listener we wrote. This event listener hides the popover if the user clicks something other than the popover and the trigger.
// Hides popover if user clicks outside of the trigger and the popoverdocument.addEventListener('click', event => { if ( event.target.closest('.popover') || event.target.closest('.popover-trigger') ) return popover.setAttribute('hidden', true)})There’s nothing we can do to simplify this event listener, so we’ll keep using it.
document.addEventListener('click', event => { // ... if ( !event.target.closest('.popover') && !event.target.closest('.popover-trigger') ) { const popovers = [...document.querySelectorAll('.popover')] popovers.forEach(popover => popover.setAttribute('hidden', true)) }})The right popover
First, we need to make the right popover and it’s trigger.
<!-- Trigger --><button class="popover-trigger" data-target="pop-3" data-popover-position="right"> <svg viewBox="0 0 40 20"> <use xlink:href="#arrow"></use> </svg></button><!-- Popover --><div id="pop-3" class="popover" data-position="right"> <p>The quick brown fox jumps over the lazy dog.</p></div>Next, we need to calculate the right popover’s left and top values.
To calculate the right popover’s left value, we need to know the trigger’s right value and the amount of space between the popover and the trigger.
function calculatePopoverPosition(popoverTrigger, popover) { // ... if (position === 'right') { return { left: popoverTriggerRect.right + space, } }}To get the popover’s top value, we need to know the trigger’s center position. We also need the popover’s height.
This top value is the same as the left popover’s top value.
function calculatePopoverPosition(popoverTrigger, popover) { // ... if (position === 'right') { return { left: triggerRect.right + space, top: (popoverTriggerRect.top + popoverTriggerRect.bottom) / 2 - popoverRect.height / 2, } }}
The bottom popover
First, we need to make the HTML for the trigger and the popover.
<!-- Trigger --><button class="popover-trigger" data-target="pop-4" data-popover-position="bottom"> <svg viewBox="0 0 40 20"> <use xlink:href="#arrow"></use> </svg></button><!-- Popover --><div id="pop-4" class="popover" data-position="bottom"> <p>The quick brown fox jumps over the lazy dog.</p></div>The left position of the bottom popover can be calculated the same way as the left position of the top popover.
function calculatePopoverPosition(popoverTrigger, popover) { // ... if (position === 'bottom') { return { left: (popoverTriggerRect.left + popoverTriggerRect.right) / 2 - popoverRect.width / 2, } }}The bottom popover’s top position is the sum of the trigger’s bottom and the space.
function calculatePopoverPosition(popoverTrigger, popover) { // ... if (position === 'bottom') { return { left: (popoverTriggerRect.left + popoverTriggerRect.right) / 2 - popoverRect.width / 2, top: popoverTriggerRect.bottom + space, } }}
That’s it!
Welcome! Unfortunately, you don’t have access to this lesson. To get access, please purchase the course or enroll in Magical Dev School.
Unlock this lesson