<!-- 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.
// Hides popover once it is positioned
popover.setAttribute('hidden', true)
We also have to add event listeners to the left popover and its trigger.
// Allows users to show/hide the popover
popoverTrigger.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 popover
document.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.
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>
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.
const 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.
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.
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.
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 popover
document.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.
<!-- 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.