Skip to content

Added closeOnEsc, hideHeaderClose & enforceFocus #247

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

Merged
merged 6 commits into from
Apr 19, 2017
Merged

Added closeOnEsc, hideHeaderClose & enforceFocus #247

merged 6 commits into from
Apr 19, 2017

Conversation

tmorehouse
Copy link
Member

Added option for disabling close on ESC (prop: closeOnEsc, defaults to true) and switched to using @keyup.esc handler.

Added option to hide header close button (prop: hideHeaderClose, defaults to false)

Added in the enforceFocus handler to make sure focus never leaves dialog while it is open. This provides better ARIA compliance as some screen readers (and browsers) will let you tab through document even though a dialog is open.

Also added aria-labeledby and aria-describedby attributes, as well as switched the modal header and footer to semantic elements for better ARIA accessibility.

Added option for disabling close on ESC (prop: closeOnEsc, defaults to true) and switched to using @keyup.esc handler.

Added option to hide header close button (prop: hideHeaderClose, defaults to false)

Added in the enforceFocus handler to make sure focus never leaves dialog while it is open.  This provides better ARIA compliance as some screen readers (and browsers) will let you tab through document even though a dialog is open.

Also added aria-labeledby and aria-describedby attributes, as well as switched the modal header and footer to semantic elements for better ARIA accessibility.
document !== e.target &&
this.$refs.content &&
this.$refs.content !== e.target &&
!this.$refs.content.contains(e.target) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing a closing )

@pi0
Copy link
Member

pi0 commented Apr 19, 2017

@tmorehouse Seems good, Thanks.

@pi0 pi0 merged commit 2303233 into bootstrap-vue:master Apr 19, 2017
@alexsasharegan
Copy link
Member

I'm having issues using the escape key to close a modal opened with vm.open(). The modal isn't in focus when it's opened. I checked my source files loaded into the project, and I'm using 0.15.0 which includes the enforceFocus method.

I've tried calling this manually with a synthetic event (since the method expects an event), but no luck. I also tried attaching a shown event handler on the modal that does the same thing bModal.$refs.content.focus(), but no luck. However, adding a setTimeout in excess of the css transitions does enforce focus on the element. This leads me to believe that the shown event is mistimed. The event is emitted at show, but the shown event should fire after all transitions have been processed.

@alexsasharegan
Copy link
Member

This seems relevant to this thread, but should I open an issue for this?

@pi0
Copy link
Member

pi0 commented May 1, 2017

@alexsasharegan Thanks for report. It would be so nice if you can create a JSFiddle / Playground and Issue reproducing same problem so we can work on it easier :)

@tmorehouse
Copy link
Member Author

tmorehouse commented May 1, 2017

Yeah, if you can generate a small file, I can look it over to see what might be going on (might be timing issue as to when the handlers are installed/fired)

@alexsasharegan
Copy link
Member

I don't use jsFiddle much, but hopefully you can see a working example forked straight from the docs.
https://jsfiddle.net/s3kc62ud/

@alexsasharegan
Copy link
Member

Looks like I did do that wrong. Take two!

jsFiddle

@tmorehouse
Copy link
Member Author

Thanks, I'll take a look at this a little later today.

@alexsasharegan
Copy link
Member

alexsasharegan commented May 1, 2017

P.S. @tmorehouse I'm totally new to Vue transitions, but I've dealt with the timing of modal shown events before. It's inherently asynchronous, and typically comes down to a listener on animationend or transitionend. I feel like Vue has some built-in goodness for that though via the transition-group apis. This is probably not new news for you, but I figured I would pass it along just in case it could save you time.

@alexsasharegan
Copy link
Member

Looks like the async shown bit can be fixed by removing your original this.$emit('shown') call on line:166 (ish) in favor on the transition component's events:
image

@tmorehouse
Copy link
Member Author

Thanks again! Will fiddle round with this in a short while.

@alexsasharegan
Copy link
Member

And for my two cents, I would consider applying your enforceFocus method without coupling it to a browser event listener. You should be able to listen to your own shown event in the mounted hook like so maybe:

mounted() {
    this.$on('shown', this.enforceFocus);

    if (this.visible === true) {
        this.show();
    }
},

So now you won't depend on an event object:

enforceFocus(e) {
    // If focus leaves modal, bring it back
    // eventListener bound on document
    if (this.is_visible) {
        this.$refs.content.focus();
    }
}

You could also make it a prop option, but I think modals should always acquire focus once they're active.

@tmorehouse, sorry if I'm blowing you up right now! I'm in the middle of a work project using this, so I'm in the thick of it. I respect your time! Get to it whenever you have a chance of course!

@tmorehouse
Copy link
Member Author

We just want to make sure if a keyboard (or screen reader) user tabs outside of the modal root element that the modal body is focused, and that the user can still tab around the contents of the modal. So we must listen on the body or document for focus events, and if the focus event wasn't within the modal that we then trigger the focus on the modal body.

@alexsasharegan
Copy link
Member

Ah, that's certainly something I hadn't considered. Sounds like my use case falls outside of the current control flow though. Maybe you can start with a check for whether or not the event object is present to determine it's not a document/body event.

@tmorehouse
Copy link
Member Author

I'll definitely tackle the onEsc issue tonight, and then review the enforceFocus after.

@tmorehouse
Copy link
Member Author

I believe I have a fix now for the onEsc glitch, by introducing the focus event on vm.$nextTick() to ensure that the DOM is present.

Also, rather than focusing the modal content on open, it now will focus the first focusable item in the modal content (first starting in the footer, then if no element found, then checks the body, and if not found checks the header, and then if not found as a last resort focuses the modal itself).

I'm going to mock up a fiddle for testing, and will update this thread later.

@tmorehouse
Copy link
Member Author

tmorehouse commented May 2, 2017

And I just realized I should have been listening to focusin not focus on the enforceFocus event listener. Dumb me, hehe

@alexsasharegan
Copy link
Member

@tmorehouse I checked in the newest release, and I'm seeing your changes in the modals. Have you fixed the "shown" event issue yet? Or more simply, of the issues mentioned here, which are not yet patched? I've come up with workarounds, so I want to eliminate all the unnecessary code I've added in my project.

Thanks for your work on all this!

@tmorehouse
Copy link
Member Author

tmorehouse commented May 6, 2017

When the modal is shown, it should focus the first non-disabled focusable item (with the search for the focusable item starting in the footer and working it way into the body, then the header, else the modal itself.

This also ensures that hitting ESC will close the modal.

For the enforceFocus, we must listen to the body/document element for the focus events, to detect if focus has left the modal (say via hitting TAB) to another focusable element within the document. If a user tabs through the modal, it will cycle through the modal's focusable content.

See current fiddle: https://jsfiddle.net/pi0/bofh9aaa/ which is using the latest release. The first focusable element found is the "close" button (in the footer)

@alexsasharegan
Copy link
Member

Cool! Yeah, I stripped my focus hack out and it works much much better! Thank you!

Are you following my issue with the shown event? If you listen to "shown", you callback is fired before the modal has completed its disappearing act.

@tmorehouse
Copy link
Member Author

Haven't moved the 'shown' event emit to the transition element yet.

@alexsasharegan
Copy link
Member

Okay, okay. Just making sure that piece didn't get miscommunicated. Again, thank you! I'm having fun using bootstrap-vue! Lots of little gems like alerts with timeout dismissals built in!

@tmorehouse
Copy link
Member Author

The only issue I can think of regarding placing the shown emit on the transition, would be if you are using @shown to clear content in the modal (i.e. in text input field), then the field wouldn't get cleared until the transition has ended, causing the user to see the old value momentarily.

THe only way around this might be to introduce a couple of new events to be emitted, such as show and hide, which are fired before the modal start to show, and before the modal starts to transition to hidden state (respectively)

@alexsasharegan
Copy link
Member

Exactly! I vote for emulating the current bootstrap events, but without all the namespacing cruft. Unfortunately for some, this could be a breaking change, but I see it as more of the expected behavior since this is a bootstrap-in-vue framework.

image

@tmorehouse
Copy link
Member Author

There are actually two "shown" events that are fired by the modal. One is fired on itself, and one fired on the app $root:

  • this.$emit('shown')
  • this.$root.$emit('shown::modal', this.id)

The second one is more akin to the namespaced events in Bootstrap, and is available by listening to this.$root.$on('shown::modal, function(...){...})

@tmorehouse
Copy link
Member Author

Currently the shown::modal event is used to detect if another modal is opening, and if so close the current modal if it isn't open.

So there would need to be a bit of refactoring of code logic to change this detection to happen before the show transition starts.

@alexsasharegan
Copy link
Member

Yes, I can see that being a bit of work. Hopefully I can attempt some contributions. Still, I find it very important. While it's probably a UI anti-pattern, sometimes it's necessary to have a modal pop up conditionally from another modal (maybe an "are you sure?" or something with terms of use). I've found the four base events necessary to properly time the multiple modal transitions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants