[MUSIC PLAYING] ROB DODSON: Hey, everybody. What's up?
It's Rob Dodson. Welcome back to the
A11ycasts show. Today, I want to talk about
modal dialogs, which I consider sort of like the boss battle
at the end of accessibility, because they're pretty
difficult to implement and there's a lot of things
you've got to consider. And today, I want to
show you how I do it, and hopefully give you some tips
on how you can make it easier to do them in your own app. So starting off with
usually, whenever I'm going to implement any
new pattern that I'm not super familiar with, I'll go
over to the ARIA Authoring Practices Guide. And I'll see what they
recommend for a pattern, and in particular
around keyboard interaction and any
ARIA roles or states that those elements need
to have applied to them. So here it's telling
me, you know, when the user presses
Tab, we've got to move focus within the
dialog, ideally wrapping focus inside of the dialog. And when the user
presses Escape, we want to close the
dialog because, you know, someone might not be
familiar with the orientation of it, they might not land
on the right Close button or whatever, they just want
to get out of the thing. We should make sure
that they can do that. And then there's a
few roles that we want to make sure we've got. So we've got a role of
Dialog on the element, and we're using aria-labelledby
to label the element, as well. So when it opens,
we move the user into it using some focus
management techniques, it just announces,
hey, this is what this thing is, it
announces its role, and then hopefully
they can move around to the immediately
focusable controls. The authoring practices
are really useful here. There's another document
I want to clue you into, as well, which is the
eBay MIND patterns. So this is a guide written by
some accessibility testers over at eBay. It's also a super useful,
very practical guide to building a lot of
different UI patterns, in particular modal dialogs. So the implementation that
I'm going to follow today is the one that is sort of
presented in this MIND patterns doc. So I'll be using a
lot of the same code. There are some places where I'm
going to deviate a little bit, and I'll kind of call those out. But both the ARIA
Authoring Practices Guide and the MIND patterns
doc are very, very useful for building some of
these UI patterns. So over here in this lifestyle
site that I've built, I'm going to show you the
modal dialog that I've created. And it's pretty simple. Basically, when I click on
this Add to Cart button, I'm going to pop open
this little window that just says, like, Dialog Example,
and has a little message here. And there's a couple controls
for either closing the dialog or saying, OK, yeah, sure, it
was added to my cart, whatever. Now, there's a few things
that are happening here that I want to sort of highlight. So notice that, when I tab down
to this Add To Cart button, and I hit spacebar
to open my modal, that focus is moved right
away into the modal dialog. So it's actually on the
first focusable element, which is this Close button. And I can hit Escape, right,
to close the modal dialog. And focus is returned to
the previous active element. So that's a very
nice pattern that you want to implement to make
sure that the user has kind of, like, a continuity
of their context, right? You don't want to
open a modal dialog and then, when the user closes
it, you just, like, blur focus, and then they have to start
over from the top of the page and work their way
all the way back down to where they previously were. Turn on a screen
reader so you can see the experience in voiceover. SPEAKER: Voiceover on Chrome.
A-D-D to Cart button. Dialog example. Buy our awesome stuff. It's really awesome. Close.
OK. Dialog Close button. ROB DODSON: Right? So it announced the
title of the dialog, it announced the controls that
were inside of it, kind of read the content. It said the role. It was kind of mixed in there. But it also said dialog, letting
the user know what they're interacting with, right. So I want to make sure
that I get that same result in my implementation, as well. And there's a fair bit of code
that I've used in this example. So rather than
build it live, I'm going to walk you
through the code that I've already
implemented, and just kind explain what's
happening as we go. So the first thing that I often
try to do with a modal dialog is, rather than mix it into
the rest of my page content, right-- so you might have, like,
a main section, and then, like, some other sections
inside of there, and sometimes I see
people just, like, drop a modal dialog right inside
of kind of like a main area-- instead what you
want to do is you want to make it kind of like
a top-level sibling to all of the other stuff
inside a body. So for instance,
you want it to be sort of an immediate
descendant of body. And what this lets
you do is make sure that all of the other controls
in the modal, or sorry, all the other controls that
are sibling to the modal, are disabled in some fashion. And that way, we can make
sure that the screen reader and keyboard focus stays
just inside of the modal, and doesn't ever
break out and get into the rest of the page,
which we definitely don't want. And so I've placed this right-- basically, right along
where my script tags are, kind of the closing body tag. I've got the implementation
for my modal right here. And that way, I
can just go through and I can make everything else
in the document inert, which I will talk about in just a sec. And let's walk
through the HTML here that I'm using for this dialog. So first, I create a div
with a role of Dialog. And I'm also going
to say that it is going to be labeled by
this element called Dialog Example, which is actually this
little h2 that I have inside of here. Now, following the pattern
from that MIND patterns doc, there's actually two
elements inside of my dialog. There's the dialog
window itself, right, which is where the OK and
the Close button goes, and then there's
also the dialog mask. So this is going to
be the overlay that sits behind the dialog. And if the user
clicks on that, that will also close
down the dialogue and return their focus
back to the page. So these are kind
of the two elements that I want to work with. Over in my CSS,
initially my dialogue is set to Display None. So all those things are hidden. And that also means
that they're not going to be in the accessibility
tree or anything like that. What I often see
people do with dialogs is they just position
them offscreen, but they forget to hide them. And so a screen reader
user is still actually able to navigate
into those things, even though they're offscreen. And you definitely
don't want that. So setting them to Display
None or Visibility Hidden is a very useful
way to make sure that they're completely removed
from the accessibility tree. Then, whenever the dialog is
getting ready to be opened, I'll just apply an
Opened class to it. And that's when we'll
set it to Display Block. And in that moment,
it gets added back to the accessibility tree. For the dialog window,
I'm using a fixed position for that element. I'm saying that I want it
to be top and left 50%, and then using a
little transform trick to kind of, like, move it back. So this is how we sort
of perfectly vertically and horizontally center
a fixed element using this little Position Fixed
and Transform combo here. And then for the
dialog mask, I'm going to also set it
to Position Fixed. And I'm just going to
make it span, basically, the entire dimensions
of the page, and set my background to
black with a 60% opacity. Now, over in my app.js
file, whenever you click on that little, you know,
Product By button, I'm just going to run a
little open dialog method. And over in my
modal.js file, here is my actual implementation
for that open dialog method. And there's quite
a bit of code here. So let's walk through this
kind of piece by piece. So the first thing I often
do is I'll just define, like, a constant for some key codes
that will be commonly used. In this case, the Escape
key is probably the only one that we really care
about for our dialog. And that's going to,
again, let the user quickly exit out of the dialogue,
and collapse everything down. And then I go through
it, and I'm just going to grab references to
a few important elements, the dialog, the mask, and
the dialog window itself. And then I've got
this variable here for the previous active element. Now, remember, the
behavior that we want is, when the user
opens the dialog, we want to retain
knowledge of what the previous active element
was so, when they close it, we can return that. So I'll go ahead and set
up a variable for that. And whenever I open the dialog-- so this is what gets run when
someone clicks that button-- the first thing I do is
I look at the document's active element, and I save that
in my previous active element variable. So that way, I can
restore it later. And then I go and-- this is
kind of a quick and dirty trick that I'm doing here. You might want to do
something a little bit more refined in your own site. But I just look at all
the immediate children of the document body. And remember, we made
sure that our dialog was an immediate child of the body. So look at all the
other sibling elements. And I'm going to set them--
if they're not the dialog, I'm going to set them
to inert equals true. So, inert. Some of you maybe have not
seen that property before. This is actually
something that we're working on proposing in
sort of the standard space. And what inert does is
it removes an element from the accessibly
tree, and it makes sure that none of its
children are focusable. There is a polyfill for
Inert, which you can use, which I have used in this site. So you go to
github.com/wicg/inert. You can also just
npm-install this with npm-install--savewicg-inert. And that will get you a
JavaScript file for the Inert polyfill. In my application, I've just
included this basically right here, right before
my modal.js script. So I'm setting all the
other siblings to Inert. So they're not going to be
in the accessibility tree. And that means that
someone can tab around, and they'll just stay
inside of the modal dialog. Then I open the modal dialog
up by adding that class. That makes our CSS go. And then I just
add a few listeners for things that should
close the dialog. So you know, someone
clicking on the dialog mask. Here, I'm kind of cheating
and I'm just saying, if you click on any of the
buttons inside of the dialog, I'll just go ahead and close it. But in your case,
you probably want to actually have some additional
actions that happen there. And then also listening
for a keydown handler. I've got a method here
called Check Close dialog. And what that's
actually just doing is, hey, if someone
hit the Escape key, let's close the dialogue. And then finally, at
the very, very end, I find the first
focusable element inside the dialog, which
is a button, in this case. And I move the
user's focus there. So I've managed the focus
into the dialog element. That's really important
to do because, if you just open the dialog and you
don't move the user's focus, then what's going to happen is
they're not even going to know maybe that the dialog was open. They're not going to know
anything is on the page because you haven't kind
of moved their context into that new element. You haven't informed
them about it. So we're moving focus
into the dialog. Now the user knows it's
there, and the screen reader should announce it as such. And then for closing
the dialog, we're basically just
going to go through and kind of de-structure all
the things that we just did. So we go remove our
event listeners. We loop through all
the sibling elements. We make sure that they
are no longer inert. We remove the Opened class. And then finally, we just find
that previous active element, and we focus it instead. So we've managed focus
out of the dialog now back to that element. And so what this gives
us is that experience that I was showing before. Here, we'll go through it
again with our screen reader. And we'll highlight some of
the focus issues here, as well. So. SPEAKER: Voiceover on Chrome. License 0. A-D-D to Cart. Dialog Example. Buy our awesome stuff. It's really awesome. OK button. Voiceover off. ROB DODSON: So that's working. Something that I
want to highlight is how the focus behavior works. So I open this, and I'm tabbing. And you'll notice
that focus is allowed to escape a little bit, right? So it actually is allowed
to move up to the URL bar. And then the user tabs, and
it moves right back down into the dialog. Some documents, like the ARIA
Authoring Practices Guide, say that should be completely
trapped inside of the dialog. Personally, I-- there's
a bit of a debate, and I kind of
question that, mainly because if you're trapped
inside of the dialog, then you're forcing the user to
take an action, which I think could be a bit of
a security issue. For instance, you might
have a malicious website that's trying to trap
a user inside a dialog and force them to, like, install
some malware or something. So I want to make sure the
user does have the opportunity to get out of the page if
they need to to close the tab. And what will happen
for a screen reader user is it will actually
announce that they're leaving the document. So we can see that here. SPEAKER: Voiceover on Chrome. Dialog Example. Buy our awesome stuff. OK button. Leaving web content. Close-- Voiceover off. ROB DODSON: Right.
So it said leaving web content. I actually started to
hit some keys there, so it didn't quite finish
what it was saying. But it said leaving web content,
and then it was going to say, you know, close
lifestyle tab, basically letting me know that I can close
this current page if I don't want it to be open any longer. So anyway, I know
it's a lot of code. And hopefully, we can have some
new standards in the future, like the dialog element
itself, which is part of HTML, but currently only shipping
in Chrome and Opera. But hopefully, some other
folks get interested in that. We also have some
lower-level primitives that we're working on to make
this easier, things like Inert and a few other things. So hopefully, in
the near future, this will be a
little bit easier. But today, if you're
building modal dialogs, this is the process
that I would recommend you follow to make sure that
your screen reader users have a really nice experience. That about covers
it for this episode. Again, if you have
any questions, you can always leave them for
me down below in the comments, or hit me up on a social
network of your choosing. As always, thank you
so much for watching, and I'll see you next time. Hey, folks. If you want to learn
more about managing focus or things like the
inert polyfill, I've got a couple episodes
that you can check out. Give those a look. As always, thanks for watching,
and I'll see you next time.
If you want to know why not to just use a
prompt()
, it willamong various other things. Also it selects "OK" by default, which may not be the desired outcome.