We are creating an accessible popup menu for a web application. It covers various concepts, including handling keyboard events, creating a new dropdown component for reuse, using arrow keys to navigate through buttons, using the Tab key to close the menu and go to the next item in the tab order, using the Escape key to close the menu and focus on the button that closes it, using the
aria-expanded attribute to communicate whether the menu is open or closed, and the difference between the tab index values of 0 and -1.
Instructor: [0:00] Now, let's convert these playback rates into a dropdown-menu. The first thing I want to do is create a new component called dropdown-menu. We'll be moving these audio-player files into their own component folder. Let's do new folder. Audio-player. We'll move these two in here. [0:20] Before we do that, let's make sure that we adjust the paths. In app.js, we want to make sure that we update to audio player. Make sure we have not broken anything. Refresh. Everything is still there. Now, in this components folder, let's create a new folder called dropdown-menu and in that folder, we'll create a new file called dropdown-menu.js.
[0:45] Now, let's just create a function component, so const DropdownMenu and we're going to start with an options prop, but we'll be adding more later. We'll make that an arrow function. For now, we're just going to return some placeholder text of a div with the word DropdownMenu.
[1:05] Now, let's not forget to export default DropdownMenu. Now, I'm going to import DropdownMenu from DropdownMenu DropdownMenu.
[1:19] Now, let's go back to the rates and let's comment this out since we'll be using it later and then we will add DropdownMenu. In the options, we'll pass in the rates. Now, we have that component in place.
[1:33] Before we start building this, I want to take a look at some of the APG examples. This is an example of how an accessible menu might look. Just note that this example demonstrates how the menu button design pattern can be used to create a button that opens an action menu. The thing that is really nice about these examples is they show you what it looks like.
[1:56] It tells you some accessibility features. It also shows you what the keyboard support is. In this particular example, there is menu button support. On the trigger of the menu, we have space, enter, down arrow, and that opens the menu and focuses on the first menu item. Then the up arrow, which opens the menu and focuses on the last menu item.
[2:18] Then on the menu itself, it has enter which activates the menu item. This is when you are actually in the menu. It activates the menu item. It closes the menu, and then set the focus on the menu button. You can see we have a few in here. We have escape, up arrow, down arrow, etc.
[2:35] Just one thing to note, this is an example. You can take a look at some of the APG menu and menu button patterns. If I'm in the menu pattern, you can see that there's some examples in here. You can see that some of the things that were mentioned, like spacebar, is optional.
[2:51] You can also see things like home if the arrow key wrapping is not supported, meaning if it's not circular. Same thing with end. Also, pressing a printable character is optional. This would mean that, if you press the P key, it would go to the first menu item that started with P. This is also optional, so we're not going to include that in this example.
[3:11] You can also look at the menu button and you can see that the down arrow and up arrow are also optional, so we aren't going to do that in this example. Now that we have a good idea of what needs to happen, let's start doing some pseudocode in that drop down menu file.
[3:24] The first thing we need is to create a state for open close menu. Need to be able to send focus to various places. We'll do a two button upon close to other menu items upon arrow keys. We navigate focus in menu through arrow keys, not tab.
[3:47] Instead of this div with drop-down menu text, we're going to make it actually function like a drop down menu. First, we are going to need a few things from react. We are going to import use state and use ref from react. I'm going to create two refs. Cons button toggle ref equals use ref and we're going to set that to . Const drop-down wrapper ref equals use ref .
[4:20] Now I'm going to create an is open state. We are going to do structure is open and set is open from use state and the initial value will be . Next, we're going to remove this drop-down menu text and we are going to create a button. That button is going to have the text of menu.
[4:40] Then you're going to add a ref to that button, and we're going to put that as the button toggle ref. You're also going to add an on click handler on the button, and we are going to pass in a function called toggle menu. Create that function.
[4:59] For right now, we are going to just set is open to the opposite of is open. Remember, we only have an on click right here. We don't need to add an on key down because we're not supporting the arrow keys. If we were, we would want to have an on-key down. On click on a button supports space and enter.
[5:20] Next, I'm going to create a div. That div, I'm going to add a ref and call it drop down, wrapper, wrap. Next, we're going to take what we just commented out of the audio player. Next we are going to paste that inside of the div. Let's uncomment it.
[5:38] Instead of rates, we are going to change this to options. Change rate doesn't exist in this component. For now, I'm going to change this to an anonymous function. I'm also going to do the same thing for on key down and create it in an anonymous function that does it. We'll handle that in a bit.
[6:21] I'm going to add a button class, a menu class, and a class name. I've already created styling with certain class names, so I am going to add those classes into my react component. For this menu class, I'm going to make it a ternary. It is open.
[6:41] I'm going to create a template literal and do menu class open. Otherwise we'll just render menu class. Now that we've added all of those props, let's pass those props into the drop-down component. Now, to make things a little bit cleaner, I'm going to go back to the top of our file, where the states are rendered, and create a playback rate state.
[7:03] Playback rate and set playback rate. Those will be structured from use state. The initial state will be one. We'll take this at playback rate function and go back to change rate. We'll update the playback rate in that change rate function.
[7:21] Next, we want to make sure we pass the change rate into the drop-down component, but right now, we don't have a prop for it. Let's go back to the drop down menu, go back up to the top, and we'll add a prop called On Option Click. Back in the audio player, let's go ahead and pass that prop, the change rate function.
[7:42] In the drop down menu, on option click, and we'll pass it. Change rate. Now, we want to make sure that we actually use that in the drop-down menu. Let's go back to the option. We're going to replace that with a function called handle option, click.
[8:01] We are going to pass that function, the option, just to make this a little bit more consistent. Instead of using rate, we will change that to be option. Now, let's create that function. Let's go up. Next we'll write an arrow function. Cont menu option-click, we'll pass in that option as a parameter, and in that, we'll do on option, click option.
[8:27] Now, this is technically working, but it's hard to tell because we have the button text still as menu and it's not dynamically updating when we change the playback rate. This is technically working and changing the playback rate, but it's not updating the text.
[8:41] Let's create a new prop called button text for the drop-down menu. Button text as a new prop. Now, back in the audio player, we will create a variable and store the button text in there. I'm going to call that text button text and we are going to just render some jsx.
[8:58] I'll create a react fragment, then I'll create a span with a class name of visually hidden. In that visually hidden span we'll call that playback rate, then we'll do another span, and then we will do the playback rate state and we'll put a little X there. Next, let's take that button text and pass it into that drop down menu.
[9:23] Next thing we have to do is we have to go back to the drop down menu and actually render the button text in that menu button. Let's remove this menu text. Next, when we refresh and we change the playback rate, you'll see it update. Now it doesn't close. We have to fix that.
[9:38] Inside the handle option-click we will do set is open to . Now when we open it and we select a rate, it will close. That's all well and good, but if we tap through this, we're replacing the tab key, so tab, tab, tab. We get to the drop-down, we open it, it's still focused on the button. We want to send the focus into the menu.
[9:59] We want to use this toggle menu function to toggle the focus as well. Remember, we want to focus on the first item when we open the menu. I'm going to create a variable called focus element. Before we toggle the is open state, we want it to focus on the button toggle wrap. That way, after we change the state, it'll focus on the button toggle wrap.
[10:22] Otherwise we want to do drop down, wrapper, wrap current, and then we want to use query selector and button. This will get the first button inside that drop down wrapper wrap.
[10:34] Next, we want to request animation frame, wrap that in on this function. Then we want to do focus, element, focus. The reason why we have to use request animation frame is, if something doesn't exist in the dom, for example, if it's display none, we want to wait until it appears before we set focus. Otherwise, it's not going to do anything.
[10:57] Let's see how that works now. I press the spacebar and it focuses on the first item, but now we can't navigate. If I press the arrow keys, we can't navigate through these options. Let's quickly just return back to the documentation to see what we need to do.
[11:12] If we go to this menu, we see that enter activates the menu item. We already have this built-in by using a button. Escape closes the menu and sets the focus back to button. Up arrow moves focus from the previous item, but if it's on the first item, it'll move it to the last item. It's kind of circular. Vice versa for down arrow.
[11:32] One last thing to note, which isn't documented here. If I'm pressing enter and I'm going through and I press the tab key, it'll close the menu and focus on the next item. Let's go back to the code. Let's create a handle option E down. You're going to path an event and an index.
[11:49] Now, before we do anything, we're going to console log E. This will help us filter out what keys we'll be looking for. Let's go ahead and path this to all of those menu buttons. Instead of this anonymous function that returns nothing, we're going to do handle option, E down. Inside of that anonymous function, we are going to path the event. We're also going to path the event to handle P down, and we're also going to path in the index.
[12:16] Let's go back to the console on the browser to check it out. Let's refresh, look at the console. Press tab, tab, tab, tab, tab, tab, press spacebar. Then I'll press the down key. Arrow down, up key, arrow up, tab key, and then Escape. Now we have all of these strings that we can use. Back in the drop-down menu, we can use this event key and add it to a switch statement.
[12:46] Switch, E key. Then we are going to create a bunch of cases. To make this a little bit easier, I'm going to create a bunch of constants. Up top I'm going to create conts tab key equals tab, conts down key equals arrow down, conts up key equals arrow up, conts escape key equals escape.
[13:20] Now, let's create a bunch of cases using those constants. Case tab key, and then we're going to make sure we put a break in all of them. Case up key, break. Case down key, break. Case escape key, break. Then default.
[13:48] Let's start with the tab key because that's the easiest. We're going to keep the tab key to be the same behavior. If you remember, when we press the tab key, when we were already there, it just tabbed out. All we have to do now is set is open, default.
[14:04] The next easiest one is the Escape key. We're going to set is open to default and we are also going to do button toggle ref, current focus. Let's see how that works. Tab, tab, tab, tab, tab space. If I press tab again, it does what it needs to do. Now, let's try the Escape key. Enter, escape, and it focuses back on the menu button.
[14:35] Let's go back to that function and outside of the switch statement, let's create a variable called conts options node list. We're going to use that drop down wrapper, ref current. This time, we're going to do a query selector all, and we are going to target that button.
[14:56] Let's start with the down key first. In theory, we could just use that index, do options node list, and use the index plus 1 and focus. What we're doing is we're getting that option plus 1 in the node list based off of the current index. We press save and go back to our audio player and refresh. Tab, tab, tab, tab, tab, space.
[15:21] It works. Well, not anymore because now we don't have an I plus 1. The options list has run out. What we need to do is create an if else. If I equals options node list length minus 1, and then I will do an else and wrap what we just did. What we want to do, if we are on the last item of that node list, is we want to focus on the first one. We'll do options, node list, Focus.
[15:59] Now, let's see how that works. Now, let's tab, tab, tab, tab, tab, space bar, and we are using the down key. Down down, down, down, and we're going in a circular motion. Now I just want to do the opposite for the up key. Let's do an if else again. If else and then in the if condition, we are going to do I equals .
[16:22] Let's start with the else since it's a little easier. We'll do options node list, I minus 1, focus. Otherwise, we are going to do options, node list, we'll do options, node list, length, minus 1, focus. Now, let's try that out. Tab, tab, tab, tab, tab, space. Now we can do it the other way.
[16:52] Before we continue on, I want to go back to the action. If we scroll down to the bottom and go to the role property state and tab index attribute, you'll see that we have a bunch of Aria attributes that we have. Aria pop up 1 indicates that the button opens the menu. Aria controls refers to the menu element controlled by the menu button. Aria expanded is added when the menu opens.
[17:16] On the menu itself, you can see that the role on the wrapper identifies it as a menu. We have an Aria labeled by, which... menu. You have a role of menu items which identifies each item. Then those items have it.
[17:32] Let's use that information we just learned and add it to our markup. First on the div that has the drop down wrapper wrap, we'll add a role. On the options, we'll add a role equals menu item. We've already added the tab index. Now let's add an Aria has pop up equals 1. Then we'll add an aria-expanded equals and we'll make this a ternary. If it is open, it'll be 1. Otherwise, it'll be undefined.
[18:03] We still have to add an aria controls to the button, but what's that going to be? For now, we will make that say ID, but ideally, we might have more than one dropdown menu on the page. We need to make sure that this ID that it will map to is unique. I'm going to use a library called unique ID.
[18:19] Once I've added this, we can create our own unique ID. At the top of the file, I'll import unique ID from unique ID, then I'll create two unique IDs. Menu, button, ID equals unique ID. We'll start it with menu- then I'll create Const dropdownID = uniqid dropdown-. Now, what we'll do is we'll add these IDs on to their respective elements, so we'll go back to the menuButton.
[18:53] We'll add an ID to the button, give it menuButtonId. Then on the menu itself, we'll ID dropdownId. In order to give the aria controls its own appropriate value, we have to match it to the ID of the dropdown so dropdownId.
[19:11] What this does is it says that this element controls the element with this ID. Last on the menu, we'll add an aria-labelledby, and then we will match it to the menuButtonId.
Hey Lindsey Kopacz, Hope you are doing well, where can I copy the css file (audio-player.css) to follow the class exactly the same.
Hey Sergio! https://github.com/lkopacz/egghead-react-a11y-audio-player/blob/14-create-dropdown-menu/src/components/audio-player/audio-player.css Here you go!
Thank you very much, I appreciate your effort! By the way, what a great course, well done.