The dropdown looks great but right now it's completely static - it's stuck in an open position. For it to be complete, we'll want to be able to toggle it open and closed.
To do that, we'll add some Vue specific that will track an isOpen
variable in the component state that we'll use to open and close the dropdown. After that we'll hook up a click handler to the button to do just that, close and open the dropdown.
Once that is done, we'll handle the case where a user clicks outside of the dropdown to close it as well as add a keyboard accessible variant to close the dropdown
Adam Wathan: [0:00] Right now, this dropdown looks great, and everything is where it's supposed to be. But when we actually click the avatar, the dropdown doesn't open or close. It's just permanently open.
[0:10] In this lesson, let's walk through using a little bit of Tailwind a little bit of Vue to actually make this dropdown work the way that you'd expect.
[0:17] The first thing that I'm going to do is add a little bit of state to keep track of whether or not this dropdown should be open. I'm going to add an isOpen property, and why don't we set it to false by default?
[0:30] Next, let's hide the actual dropdown area whenever isOpen is not set to true. I'm just going to add a v-if here and have this set to isOpen, so whenever isOpen is true, this is going to be displayed, whenever isOpen is false, it's not going to be displayed.
[0:45] Now you could see the dropdown did actually vanish. If we were to switch this to true, it should default to being open. We do want it close by default, so let's switch this back to false.
[0:54] Next, let's make it open when we click the button. We're going to add a click handler to the button. We'll say @click and then we'll just say isOpen is equal to not isOpen. We'll have this button toggle that any time that you click it.
[1:09] Now, we should have this working. If we click this, it opens. If we click it again, it closes. We're off to a good start here.
[1:15] Now, the next thing that you probably want to happen with a dropdown like this is you want to be able to click outside of the dropdown and have the dropdown actually close.
[1:24] Right now, if we click outside, nothing happens. We can just select text, and stuff like that, but the dropdown stays open.
[1:29] Now, there are a couple of different ways we could solve this problem. One of them is a lot more JavaScript-heavy than the others. I'm going to focus on an approach that's a little simpler and relies more on working with the DOM and CSS, and stuff like that.
[1:41] What we want to happen is we want to be able to click outside of this dropdown and have this close. We need some sort of click handler on some element to trigger closing the dropdown.
[1:51] What if we added an element that filled the entire screen but sat behind this dropdown? Whenever you clicked outside of the dropdown you were clicking this full-screen overlay that closed the element. I'll show you what I mean.
[2:05] What I'm going to do is I'm going to start by adding a button. Because we want this to be clickable, the semantically correct thing to do here is use a button element, not just a div with a click handler, or anything like that. I'm going to add a button.
[2:18] What we're going to do is we're going to add a click handler to this button that says, "Whenever you click this, isOpen is going to be set to false." We don't want it to toggle, we just want it to close.
[2:28] Now you can see that we do have something happening that's affecting the layout here. Let's use some Tailwind utilities to make this button act as full-screen overlay.
[2:37] To start, let's just add a class attribute here. You might think to start by adding a position absolute so that we can position this element wherever we want.
[2:46] We could do something like top-, right-, bottom-, left-, and you think maybe this is going to cover the entire screen now.
[2:54] Let's give this a background color, like bg-red-500 or something so we can see it. You can see it's still actually not showing up. In this case, let's also add a height of full and a width of full and see if that works. Now we do have this square showing up.
[3:09] The thing that you might be surprised by at first is that this button is only covering the space taken up by the avatar. It's not filling the whole screen. This comes back to what we talked about before, where because we have added position relative to this div, it's acting as a position reference.
[3:26] Any elements nested inside that have position absolute are going to be positioned relative to the nearest positioned ancestor.
[3:34] If we want to completely break out of this container, we can use position fixed, which makes it always relative to the viewport. Now you can see we have this giant red square that covers the entire screen.
[3:45] We don't want this always visible. We only want this visible when the dropdown is open. Just like this div, we can add a v-if to this as well. We can say v-if isOpen, show this square, otherwise don't.
[3:59] Now if we go ahead and click this button, we should see that big red rectangle show up again. It does. If we click the red rectangle, it closes.
[4:07] We don't actually want this to be this ugly red rectangle, of course, because that's not what you'd want a real UI to look like, but maybe just so we can see the effect of what we're doing here, let's keep a little bit of a color.
[4:17] Why don't we do something like maybe a bg-black but give it an opacity of, say 50, see what that looks like, a 50 percent? If we click this button, and now everything kind of dims except the dropdown. You may like this effect. You may not like this effect.
[4:31] We could, of course, make this totally transparent. I think for the purposes of understanding what we're doing in this lesson, it's nice to have some visual representation of this area. I'm going to keep it this color.
[4:42] One thing you may be noticing is that we're getting like a pointer cursor when we hover over the background, which may be a little bit unexpected. I'm going to switch this to cursor default instead of the pointer cursor. That way, it doesn't actually look like you're trying to click on a button, even though that's how it's implemented.
[4:58] Now you see we do just get the regular cursor, but we do get like the pointer cursor on the links and stuff like that, which I think is more like how you'd expect this to behave.
[5:06] You might notice that when we hover over the avatar button, we still have the default cursor now, not the pointer cursor like you might expect. That's because this full screen overlay is actually covering that avatar.
[5:18] Maybe this is fine. Maybe you don't care, but if you did want this button to pop through and be part of this layer, we can do that using z-index utilities.
[5:28] What I'm going to do is I'm just going to head over to the actual button with the avatar in it. To start, I'm just going to try adding a z-index, anything above zero. The first step in Tailwind by default is z-10.
[5:39] If we save this, you might expect that now, "OK. This does just show up on top of the background," but you'll see that it doesn't. The reason for that is that you can't apply a z-index to any element that has position of static, which is the default position of elements in a browser.
[5:54] If we wanted the z-index to work here, we're going to need to change the position to something else. The best option for us in this case is relative because that means the element is still rendered exactly is it would be if it was positioned static, but now it's allowed to be affected by z-index.
[6:10] You can see that when I saved, this button did pop up and appear on top of the background. If I click this, you can see the button is still there. When I hover the mouse over it, the cursor does change to the pointer cursor.
[6:22] Another little detail that might be worth fixing here is because we've implemented this background as a button, that means that we can tab to it. You can't really see this, but when I hit Shift + Tab in this case to tab back, right now the button actually has focused even though that's not visible in any way.
[6:39] That means I can hit space and close it, which is weird, not the behavior you'd expect. I'd like to be able to tab from this button directly to one of those links, not have a step in between.
[6:50] What I'm going to do is I'm going to add a tab index of negative one to this button so that it's inaccessible from the keyboard.
[7:00] Now, if were to go ahead and open this dropdown, when I hit tab, it goes directly to the link and directly back to the button. We skip that background section.
[7:09] Now, one final piece of little clean up that I'd like to do related to these classes. You remember here I introduced this top-, right-, bottom-, left-. This can be a little cumbersome to type out all four of these utilities when you just want to set all of the sides to zero.
[7:24] Tailwind has a shorthand for this using the inset utility. This is based on upcoming feature of CSS where they're going to be adding a native inset property that works as a shorthand for these four properties.
[7:37] We can just say inset-, and then we don't need any of this stuff and this just does the exact same thing. You'll still see that when I click this button, we get that full screen overlay.
[7:47] Just like margins and paddings and stuff in Tailwind, if you only wanted to do this in one direction, you could do inset-x- or inset-y-, which again is just a long form for inset-, but this is the left and right directions and this is the top and bottom directions.
[8:03] Now, in our case, again just inset- is totally fine, so I'm just going to switch it back to that.
[8:08] Now, one final thing in terms of the behavior of this component.
[8:11] You might have been saying to yourself after we made this button not keyboard accessible by adding this tab index, you might have been thinking, "Whoa. Whoa. Whoa. We need to be able to close this thing with the keyboard somehow. That's inaccessible." That's true. We should add a way to make it possible to close this dropdown from the keyboard.
[8:27] Now, the most conventional way to do that though is by hitting the escape key, not by tabbing to some invisible button in the background and hitting space.
[8:36] This is not Tailwind-related at all. This is just going to be a totally JavaScript Vue thing, but I thought just for completion-ness sake, I'll add this in here for anyone that's interested. Let's quickly walk through how to set this up so that when we hit escape, this will close.
[8:51] What I'm going to do is I'm going to add a created hook to the Vue component. In here, we're going to create an event listener function that's going to handle any escape key presses.
[9:00] I want to call this handle escape and it's going to receive a keyboard event. Then the first thing we're going to do is we're going to check if the key that was pressed is the escape key, or not to support other browsers.
[9:14] We also have to check if it could be the full word escape, not just esc. If it's either of those, then we want to say isOpen should be false. Anytime you hit the escape key, we're going to close this dropdown.
[9:28] All we need to do is add this event listener to the window. I'm going to say documents.addEventListener. Now, we're going to listen for the key down events. We're going to add our handle escape listener.
[9:42] Then to make sure that we clean up after ourselves, I'm going to listen for Vue's destroyed hook, or the before destroy hook. Doesn't really matter in this case. Then I'm just going to make sure that whenever this component is torn down, that's we remove the event listener.
[9:58] I'm going to say remove event listener, key down. We're going to remove that same event listener. Again, really simple. All we're doing is adding an event listener to the document.
[10:08] Anytime a key is pressed, we're going to run this function. If it's the escape key, we're going to make sure to close the dropdown. We're going to make sure to remove that event listener to prevent any memory release or anything anytime this component is destroyed.
[10:20] With any luck, if we open this, I should be able to hit escape on the keyboard, and yup, it closes.