-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add <Dialog> element #1416
Add <Dialog> element #1416
Conversation
I'm anticipating what Jamie would say if he were here: "Is it not possible to achieve the same thing using stateless components?" |
@rupertbates I can't think of a way :( FWIW react's own section on a11y asks you to use props and state, notably being the 1 part of the react site that doesn't tell you to not use them |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work!
Seen on PROD (merged by @walaura 10 minutes and 40 seconds ago)
Sentry Release: support-client-side, support |
✅ Testing in PROD passed! Details |
<ControlledDialogButton modal={false} /> | ||
</ProductPageTextBlock> | ||
</div> | ||
)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why make this an option available through props? Non-modals are fiddly and require more complex focus management. Can you think of a use case for it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good shout! I was thinking of stuff like the account menu but the more i think about it even that should be "modal" (you'd click anywhere on the background to close it but the page below doesnt receive that click – like context menus on mac)
We actually discourage against its use in the stories, I think it's wise to remove it until a usage scenario presents itself
className={classNameWithModifiers('component-dialog', [modal ? 'modal' : null, open ? 'open' : null])} | ||
aria-modal={modal} | ||
aria-hidden={!open} | ||
tabIndex="-1" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The WHATWG spec states that
The
tabindex
attribute must not be specified on dialog elements.
Not sure why though. Is this necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, since browser support for dialog
is fiddly at best I have decided to implement focus management cheaply by just focusing the whole dialog after it opens – this seems to net sensible results in all tested browsers.
The native behaviour would be to focus on the first interactive element – however that requires more DOM fiddling, i'm not against exploring that solution but I'm not confident it's as portable as focusing the whole thing :(
I found healthy debate here about this all, my takeaways are:
- The guidance refers more about a numberical tabindex than the magical
-1
and0
(becausedialog
s live outside of the element to a level) - Some user agents make
dialog
focusable? (thinking about usingtabindex=0
- Focusing the dialog itself vs focusing the first interactive element is an open discussion?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome, thanks for sharing the discussion, really interesting!
The guidance we received from the accessibility consultant:
- if there's an obvious candidate to receive focus in the dialog, use that, OTHERWISE
- provide focusable empty divs at the top and bottom of the dialog and use JavaScript to return focus to the top div when tabbing out of the bottom div (and vice versa).
I can link you the notes he provided if you need it. But there's more than one way to peel a potato, and as long as the approach is tested, it's all good! 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we do have a naively implemented oneline focus trap at the end but not one at the top 😢 I'm not sure how to implement both sides without a lot of code. it's something i want to look into though!
<dialog // eslint-disable-line jsx-a11y/no-redundant-roles | ||
className={classNameWithModifiers('component-dialog', [modal ? 'modal' : null, open ? 'open' : null])} | ||
aria-modal={modal} | ||
aria-hidden={!open} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we also include the open
attribute?
EDIT: I have just seen that the open
attribute is provided by the wrapper. Is there a reason it is defined in the wrapper and not here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
heya! it gets opened using the js callbacks in the lifecycle hooks a bit higher up the component. You can't actually use both. if you have open="true"
and try to fire up dialog.show()
that's a javascript warning, makes for a lovely imperative vs declarative debate!
(Also, open="true"
doesn't support modals at all 😝. I think the usage scenario is for serving markup from a server. Even the MDN demos don't set it via js)
As all <dialog>
elements support dialog.show()
I chose to omit the open attr altogether and instead style the polyfill with traditional css
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting. And so confusing! I'm sure it's contrary to what is outlined in the spec. Browsers are a law unto themselves 🙄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
aria-modal={modal} | ||
aria-hidden={!open} | ||
tabIndex="-1" | ||
role="dialog" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm guessing this is needed for browsers that don't support the <dialog>
element?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes :(
close() { | ||
if (this.ref && this.ref.close) { | ||
this.ref.close(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens to the focus after the modal is dismissed? Ideally it would return back to the button that opened it, or perhaps returned to the beginning of the page
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
alas it depends on the implementer. the dialog does fire an event when it closes, so the component that fired it should be able to return the focus. In my testing it would seem that chrome makes a decent job at doing this on its own.
Something we could do could be to tweak the API so there's a nullable but required returnFocusAfterClose()
prop to make it explicit that if you implement a dialog you have to take care of that? But I want to test default browser behaviour a bit more in case that just adds extra work
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, sounds like it would work!
As an aside, it's worth testing the following combos of browsers and screenreaders, as these are the most commonly used:
- IE11 + JAWS
- Firefox + NVDA
- Firefox + JAWS
- Safari + Voiceover
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great! 💎
Sorry for the post-merge question spam. Sources for my questions:
@SiAdcock not at all! you raise many super valid points and have given me food for thought, will tag you in a subsequent PR. |
Why are you doing this?
We need an accessible (as possible) way to display dialogs that exit the page flow, float over it, and can be dismissed. This component wraps a native
<dialog>
element (which should take care of a11y for us) and polyfills it as much as possible.There's some barebones styling but the idea is that every dialog client would implement its own styling.
Tested it with voiceover in Chrome, Safari & Firefox, as well as with my own keyboard. A known issue is that the focus trap only works going down, this is for simplicity reasons in this PR, implementing a better focus trap can be done without altering the public interface.
Trello Card
Screenshots