 Hey there, folks. Rob here. Welcome back to the show. So today, I want to talk a little bit about how we do keyboard support in custom controls. Follow me over here to my laptop. And I want to show you this radio group that I've been working on. So on my page here, I've got a regular just native text input. I've got some custom radio controls that I've created. And I've got a regular native button down here at the bottom. And I want you to watch the focus behavior that happens here. So I'm going to hit Tab. And that's going to focus my text input. I'll hit Tab. And that will move focus into my radio controls. I can press down or up to focus different items inside of there and select them. And I hit Tab again to move out to that button. I can also hit Shift Tab. And when I move back to my radio control, my focus lands on the previously selected item. And I want you to look at what's going on over here in the DevTools as I do this. So I've got this current item selected. You can see it's Tab index is set to 0. And when I press the up arrow key, now the element that came before it's Tab index is set to 0. And its Tab index is set to negative 1. If I hit up again, we're actually going to wrap around to the bottom of the list. So our last element is now Tab index 0. All the others are negative 1. So this technique that I'm showing here is sometimes referred to as roving Tab index, or roving focus. And the idea is you want your users to have as efficient an experience as possible when they are navigating your application, especially if they're using a keyboard. So you want to treat complex controls whenever possible as a single tab stop. So the user can tab into it. They can use their arrow keys to interact with it if they would like. Otherwise, if they want to just sort of bypass it, they can hit Tab again to move to the next control. If they were interacting with that custom control that you built, they tab away, they tab back, you want to restore the focus to whichever item they had previously activated. So to do this, we're going to use this roving Tab index technique where we basically are kind of creatively toggling Tab index 0 and negative 1 on our children to give our users kind of a nice continuity to their experience. So follow me over here to my code editor, and I'll show you how we did this. Now you may have noticed that the HTML that I was using here, these were not native tags or something you might be familiar with. I actually defined my own HTML elements. And I'm doing this using the new custom elements spec. So these are custom tags that I have created, which inherit from native HTML element. I'll give you sort of an example here. So looking at this first radio button that I've created, let's go and look at its JavaScript. And this is kind of the typical boilerplate to create a custom element. So I've got a class called radio button, which extends HTML element. So I'm extending the native HTML element. This is the primitive class that all HTML tags inherit from. I've got a constructor here where I'm not really doing anything. I'm just calling super to make sure that I've properly set up my parent class. And then I've got this connected callback. And every custom element has a set of lifecycle callbacks that you can hook into to run your own code during various phases of that element's life. So for instance, connected callback gets run every time our element is inserted into the DOM, either via JavaScript or the parser hits it, maybe because it's already in our markup, and it goes through and it upgrades it. And the first thing it's going to do is it's going to call this callback. So this is a great place for us to stamp out our templates or query the DOM for something, for instance, because we know we're actually in the document now. So in this case, what I'm doing with these radio buttons is I'm just going through and I'm sort of setting up some initial state. I'm setting a roll of radio, and I'll talk about where that comes from in just a minute. I'm setting the initial tab index value to negative 1 because I don't necessarily want this thing to be clickable. I know that it's going to be part of a larger set. So I'm going to set its initial tab next to negative 1. And I'm also going to set this ARIA checked value to false. So where did those rolls and ARIA checked and all that stuff come from? Well, if we go and we look at the ARIA authoring best practices doc. And I've referred to this in a few prior episodes. This is a super, super useful docket that just sort of lists out design patterns, UI patterns, and basically the keyboard support and the ARIA support that they need to have. So over here, I've gone ahead and I found radio group inside of this document. And down at the bottom, it tells me the ARIA roles and states that should be supported by a radio group. So it says that our outer container should have a roll of radio group, which we'll get to in just a sec. But for individual buttons, they should have a roll of radio. And when they are selected, they should be set to ARIA check true. If they're not selected, they should be ARIA checked false. So in this case, our radio is defaulting to ARIA checked false. It's got a roll of radio. So it's kind of set up. And then we're going to let that parent control really dictate its state. The other thing that's really useful about this doc is it'll tell us the kinds of keyboard support that a radio group should have. So for instance, it tells us that if the user is focused on this control and they hit the up arrow or the left arrow, that it should move focus to the previous radio in the radio group. So imagine you had the center one focused or something. Someone hit the up arrow. You'd want to go to the previous one. You want to go up one. If they hit the down arrow or the right arrow, you want to move down. What's interesting, though, is that it also says that if focus is on the last item and you hit down, focus should wrap around to the top. Similarly, if focus is on the first item and you hit up, should wrap around to the bottom. So we're going to accomplish all that using this roving tab and x technique. Let's go and look at the parent element, the radio group. And this one's got a fair bit more code to it. But it's still, I don't want you to get overwhelmed. There's some of the stuff that you can ignore if you want to. The main thing we're going to focus on is just that up and down arrow key behavior. So what I've done is I've gone ahead and I've defined a few constants for the key codes that we're going to care about. So I need to know when the user hits the left key or the up key, the right key or the down key. Because this is a custom element, I'm just extending from HTML element, just like we were doing with radio button. Again, we're just sort of setting up our constructor. And then in our connected callback, we're setting some initial state. So we're setting the role to radio group. And this is going to make sure that our screen reader announces this control properly. I'm using a query selector here to query all of the radio button children. And I'm keeping those inside of a little property I've got called radios. And then there's a little bit of code here where this is just me kind of getting a little fancy for a second, where I'm just checking to see, did anybody create a selected attribute on this tag? And maybe specify an index for one of the radios that they would like to have selected in advance. And if they did, then we're going to do a little setup work here. So we basically have a selected attribute that is mapped to a selected property. And when that changes, we'll just which item currently has the right tab index, which one is marked as already checked true. So let's go ahead and actually define this selected property in a bit more detail. What I'm going to do, since we're working with a class, is I'm going to create some getters and setters. So getters and setters, if you're not familiar with them, these are functions that you can define on a class that basically dictate how properties on that class work. So for instance, when someone sets this property, if they were to say radio group dot selected equals some number like two, it's actually going to run this function. And inside of this function I can do all sorts of cool interesting stuff to react to that change. Similarly, if they ask for that value, someone says console log radio group dot selected, then it's going to run this get function. And here what I probably want to do is just return my selected value. So what I'm going to do is I'll actually have a stored value, which will be called underscore selected. And whenever someone calls set selected or get selected, we'll actually be manipulating this underlying value. So when someone gets the value, the color getter, we'll just return whatever selected currently is. When someone sets selected, now we actually want to react to some changes here because we probably want to change which radio button is currently active. So I've got a bit of code here and I know it seems like a lot, but hang in there with me for just a second. The first line here is just checking to see if we already have a selected property. So this is actually calling our getter. It's saying, is anything already selected? The getter could return some index, it could return zero, if maybe the very first radio button was selected. And in that case, because zero is a falsi value, we want to check that is actually the number zero. So we can use this is finite method to see, okay, did we get a numeric value back from selected? If we do, that means that we need to move the tab index from one of our radio children to the next, okay? So we'll say, all right, the previously selected item, let's just look in that list of children that we have which is stored in this dot radios. We'll grab it by its index, the selected value. We'll set it to negative one and we'll remove its aria checked state. Then for the new value, right? Because again, someone's calling our setter here. So they've passed in some value. They've said, you know, radio group dot selected equals two. For the new value, now that we've set the previous item to negative one, we'll say, all right, find that new element at that index, set its tab index to zero. So now it is in the natural focus order, focus it, call its focus method and then set its aria checked state to true. I'm also gonna just like reflect the selected value, the selected index back to my radio group container which is just kind of like a nice thing to do. And then I will set the underlying sort of private selected value to that index. So again, that way if someone calls our getter later on, it'll return that value to them. Now we know how to set the selected value for our control. The next thing we need to do is modify that value using our keyboard. So when someone presses up or down, we wanna increment or decrement the selected value. So up here, I've got a key down listener, right? You can see I sort of set that up up here in my connected callback. So I'm listening for key down events and I've got a pretty simple little switch statement here where I'm just checking the key code using those constants that I defined up here at the top. So if someone hits up or left, then I wanna take one action, I wanna basically decrement the currently selected item. If they hit down or right, I wanna increment the selected item. So I just got a little bit of code that I've already written. So to decrement the selected item when they hit up or left, we're actually going to start by preventing the default event behavior because we don't want them, when they hit the up key, we don't wanna scroll the page up. We actually just wanna sort of handle that inside of our control. We'll figure out what the selected value is. We're calling our getter there. If it's zero, meaning we're at the top of the list, then we're actually going to wrap around to the bottom of the list. So we'll just set selected equal to, you know, whatever the length of our children is, minus one because this is a zero indexed array. So we'll just set the selected value to the last child index. Otherwise, we can just, you know, decrement it. We'll just remove the one value from the index. We're gonna do a similar thing when someone hits down or right, but this time we will be incrementing. So we're gonna say, all right, again, prevent default. This time our little safety check here is to see are we on the last item? If so, we'll set this selected to the first item. Otherwise, we'll just increment it. I think we're actually ready to try this out. So we go over to our browser, refresh the page, and we'll keep an eye on our tab indexes here. So I'll tab down to that control, right? We've got our first element with the tab index zero. So it's focused at the down arrow key. And now you can see that we are incrementing our tab index here. So you can kind of watch in the console and you can kind of watch over here in the browser at the same time. So I'll be incrementing, incrementing, incrementing. We get to the very last item. We hit down again, and it's gonna wrap around to that top item. So, you know, we can also go backwards. We can hit up. I'll wrap around to the bottom. If we need to, we can always tab out of this control and tab back in and focus remains on that previously selected element. So that about covers it for today. Roaming tab index is a super useful technique to have in your back pocket if you're building custom controls. Maybe you're building something like a tree view where you don't have a native browser analog. So you've got to go custom there, but you want to make sure that your user has a really good keyboarding experience. If you have any questions on what we did today, you can always leave them for me down below in the comments or you can hit me on a social network of your choosing. As always, thank you so much for watching and I'll see you next time.