I am trying to make some collapsible accordion containers on my website accessible, but I am running into an issue.
The accordions are controlled by link elements on the page - this way, a keyboard-only user can tab to them and access them. The first issue I ran into was that if a user tabbed to one of the links, the page wouldn't always scroll up to show them which one they had tabbed to. I fixed this issue setting the focus using the following code, which scrolls the link to the top of the viewport:
$(".accordion .accordion-item .accordion-heading a").focus(
function()
{
$('html:not(:animated), body:not(:animated)').animate({
scrollTop: $(this).offset().top
}, 250);
}
);
The problem I am encountering now is that when a mouse-user clicks on the link, it jumps to the top of the page and does not open the container unless the mouse-user clicks the link again.
Is there a way I can set the focus code above to only fire if the link has been tabbed to? Or, is there a better way of handling the focus issue so that it works for both keyboard-only and mouse users?
Thanks!
Firstly a quick apology, having now seen your accordion is built correctly, links with in-page anchors are actually preferable if the accordion is constructed using javascript on page load and falls back to just a list of in page anchor links and content between them.
I am that used to seeing <a href="#"> on accordion openers and weird accordion implementations I jumped to conclusions, change it back from <buttons>!
Fixing your problem
Probably not the answer you are looking for but remove the .focus() function entirely.
It produces strange behaviour where if I have one accordion item open and i tab back with Alt + Tab quickly scrolling can be really confusing as it jumps around if you tab quicker than the scroll.
One of the golden rules of accessibility is to only adjusted the scroll position on a page if it is expected (i.e. a return to top button or using in-page anchors).
In the example and on your website once I disabled the 'scroll to top on focus' the site actually behaved as expected.
I understand why you did it as occasionally a link that is focused appears off the page, however this remedies itself when you tab again or by scrolling down (your site is logical so that if I tab and my focus is not visible I know it is off the page.)
This tends to happen (items not scrolling into view) when the item is just out of sight, by a px or two, it is common and ironically now falls into 'expected' behaviour (another rule, follow accepted and expected behaviour when designing components and pages).
If you really want to fix it
In your focus function instead of just scrolling to the top of the page whenever an item is focused, check if it is off the page.
Below is an example function I found (not tested) that you can use to check if the item is in the viewport, if it is then don't do anything, if it isn't then do your scroll function.
var isInViewport = function (elem) {
var bounding = elem.getBoundingClientRect();
return (
bounding.top >= 0 &&
bounding.left >= 0 &&
bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
bounding.right <= (window.innerWidth || document.documentElement.clientWidth)
);
};
So roughly (yet again haven't tested that the correct items are passed in, this is just to give you an idea).
$(".accordion .accordion-item .accordion-heading a").focus(
function()
{
if(isInViewport(this) === false){
//item is not in the viewport so scroll it into view
$('html:not(:animated), body:not(:animated)').animate({
scrollTop: $(this).offset().top //I would perhaps add a couple of hundred pixels here to make the item appear in a more natural area.
}, 250); //remove the animation as a further accessibility improvement, animations can be off putting to people with motion or anxiety disorders.
}
}
);
This fixes your problem as no mouse user will ever be able to click an item that is off the page so they won't ever trigger the scroll event that causes the focus issue.
You can change the event setting: Instead focus() event you will do a click() event: When you click a link, you'll scroll up. This will solve the problem of both keyboard navigation and mouse clicking;And this is also more true in terms of accessibility.
$(".accordion .accordion-item .accordion-heading a").click(
function(e)
{
e.preventDefault();
$('html:not(:animated), body:not(:animated)').animate({
scrollTop: $(this).offset().top
}, 250);
}
);
Don't forget to change the link setting to a button by adding role=button attribute.
and add aria-expanded attribute.
Related
I am trying to implement a document navigation, similar to that on the Bootstrap site for their documentation. I've got it working on the whole, but there is one little aspect of it which is bugging me.
Like on Bootstrap, I am using a combination of the affix.js and scrollSpy.js, and this is working. See the following
JSFiddle.
$('#doc_nav').on( "click", "a", function( e ){
// e.preventDefault();
var target = this.hash;
var $target = $(target);
var offset = $target.offset().top;
console.log(offset);
offset = offset - 100;
console.log(offset);
$('html, body').scrollTop( offset );
});
With the e.preventDefault() commented out, the behavior of the navigation menu is as expected. Scrolling down the window results in the displayed div being highlighted on the menu. Clicking on an item in the menu list, takes you directly to corresponding div on the page and when you then scroll away from that section on the page, the menu updates accordingly.
On my 'real' page, I have a fixed header of height 100px. So currently, when I click on a menu link, the pages jumps to the required section and places it at the top of the page where the header obscures it slightly. The scrollTop part of the code above doesn't seem to work unless I use e.preventDefault(). If you uncomment this line in the fiddle and run it again, click on 'Heading 3' link on the menu, and you will see that it now puts the Heading 3 content in the page offset by 100px from the top. Perfect.
However, now scroll back up the page towards the top. You will see that the 'Heading 3' list item, remains in its :hover state, even when the mouse is no where near it. Its as though the e.preventDefault() has prevented the browser from detecting that the mouse is no longer hovering on the item.
Mouseclicking anywhere outside the browser window, corrects the problem.
Can anyone shed any light on what I'm doing wrong here? How can I prevent the default behavior of the anchor click so I can control the page scroll placement, without stopping the correct CSS painting in the process?
The problem arises because I am preventing the browsers default behavior, using e.preventDefault(), as I want to control the scroll to the anchored element.
I've tested this in Firefox and IE10.
The issue is not the :hover state, but the :focus state.
When you click on a link, that link gains focus so you apply the :focus styling (that is the same as the :hover styling in your code). By preventing the default behavior, that link stays active and doesn't lose the focus.
One quick solution would be to unfocus/blur the element by using: $(this).blur().
In your code it would look like this:
$('#doc_nav').on( "click", "a", function( e ){
e.preventDefault();
var target = this.hash;
var $target = $(target);
var offset = $target.offset().top;
console.log(offset);
offset = offset - 100;
console.log(offset);
$(this).blur();
$('html, body').scrollTop( offset );
});
You can see a demo here: https://jsfiddle.net/z32rpe3b/30/
Why did it work fine with e.preventDefault() but incorrectly without it?
The answer to this has to do with the order of execution. The onclick event happens before the href redirection happens in the browser:
Without the e.preventDefault(), the code is executed, and the browser scrolled correctly to the target offset - 100 position (in the onclick), but then it executed the link href and scrolled to the target offset. It just happens so fast that it seems that it goes directly to the target position.
With e.preventDefault(), the code is executed (scrolling to offset - 100), and the browser doesn't execute the href action (so it stays at offset - 100).
I would like to have a widget on a webpage containing a number of tabs. When the user scrolls the page and the widget comes in to view and he keeps scrolling down, the tabs should be activated one by one (without the page scrolling further down). Once the last tab is showing, the page should resume scrolling as usual. Is this doable using JS/jQuery?
UPDATE:
Since this seems too broad a question:
The problem is, I don't know how to use the scroll offset and prevent the page from scrolling down until I decide it can resume its normal behavior
UPDATE 2
I created This fiddle,
$(document).ready(function(){
$('#tabbed').mouseover(function(){
$(this).focus();
}).scroll(function(){
console.log("scrolling tabs");
});
$(window).scroll(function(evt){
var scrollPos = $(this).scrollTop()
console.log(scrollPos);
// BULLETPROOF WAY TO DETECT IF THE MOUSE IS OVER THE
// SCROLLABLE DIV AND GIVE IT FOCUS HERE?
});
});
it contains a long page and a scrollable div among its contents. The only problem is that the div starts catching scroll events only if I move my mouse. If I could find a bulletproof way to activate the scrolling div whenever the mouse is over it I'm there. Any ideas?
You can't prevent scrolling with javascript. Using iframes and divs with scroll will only work if the mouse is over them.
You can cancel the mouse wheel and keys events related to the scrolling, however the user will be able to scroll using the scrollbar (more here).
Another approach is leaving an empty area and fixing your widget inside this area, like in this working example
$(window).bind('scroll', function()
{
var scroll = $(window).scrollTop(),
innerHeight = window.innerHeight || $(window).height(),
fooScroll = $('#fooScroll'),
emptyArea = $('#emptyArea'),
offset = emptyArea.offset(),
fixedClass = 'fixed';
if(scroll > offset.top)
{
if(scroll < offset.top + emptyArea.height() - fooScroll.height())
{
fooScroll.addClass(fixedClass);
fooScroll.css("top", 0);
}
else
{
fooScroll.removeClass(fixedClass);
fooScroll.css("top", emptyArea.height() - fooScroll.height());
}
}
else
{
fooScroll.removeClass(fixedClass);
fooScroll.css("top", 0);
}
});
Then you can change the tabs while the page is scrolling.
You should be able to do this. You can use the jQuery scroll event to run your own code whenever the user scrolls up or down. Also, so long as you call e.preventDefault() whenever the scroll event is fired, you can prevent the whole window from scrolling up or down.
This question already has answers here:
Prevent scrolling of parent element when inner element scroll position reaches top/bottom?
(32 answers)
Closed 9 years ago.
I have a div that is scrollable, but whenever you reach the bottom/top of it, it begins to scroll the entire page. That could be annoying for users who scroll fast, and then the entire page starts scrolling unexpectedly.
I need something where if you are hovering over the div, the page is not scrollable.
I have tried this by adding CSS when I hover the div...
body {
overflow:hidden;
}
...It works but there is one problem. The scrollbar disappears and that looks kind of stupid to have it disappearing/reappearing. Any way to achieve the same effect but keep the scrollbar visible? I have seen it done with Facebook chat.
Here is a very simple way to stop the propagation with no plugins, just jQuery.
Update: The code has been updated to work correctly in IE9+. Have not tested in previous versions.
First, create a class on your <div> to mark it as having this behavior. In my example, I use the class .Scrollable.
<div class="Scrollable">
<!-- A bunch of HTML here which will create scrolling -->
</div>
The jQuery to disable is:
$('.Scrollable').on('DOMMouseScroll mousewheel', function(ev) {
var $this = $(this),
scrollTop = this.scrollTop,
scrollHeight = this.scrollHeight,
height = $this.height(),
delta = (ev.type == 'DOMMouseScroll' ?
ev.originalEvent.detail * -40 :
ev.originalEvent.wheelDelta),
up = delta > 0;
var prevent = function() {
ev.stopPropagation();
ev.preventDefault();
ev.returnValue = false;
return false;
}
if (!up && -delta > scrollHeight - height - scrollTop) {
// Scrolling down, but this will take us past the bottom.
$this.scrollTop(scrollHeight);
return prevent();
} else if (up && delta > scrollTop) {
// Scrolling up, but this will take us past the top.
$this.scrollTop(0);
return prevent();
}
});
In essence, what this does is to detect which direction the scrolling is being requested in (based on the originalEvent.wheelDelta: positive = up, negative = down). If the requested delta of the mousewheel event would move scrolling past the top or bottom of the <div>, cancel the event.
In IE, especially, scrolling events which go past a child element's scrollable area then roll up to parent elements, and the scrolling continues regardless of the event being canceled. Because we cancel the event in any case, and then control the scrolling on the child through jQuery, this is prevented.
This is loosely based on the way that this question solves the problem, but does not require the plugin, and is cross-browser compliant with IE9+.
Here is a working jsFiddle demonstrating the code in-action.
Here is a working jsFiddle demonstrating the code in-action, and updated to work with IE.
Here is a working jsFiddle demonstrating the code in-action, and updated to work with IE and FireFox. See this post for more details about the necessity of the changes.
maybe have a look to
How to disable scrolling temporarily?
This is a sample to stop and activate scroll
I have several <input type="number"> elements on my webpage. I'm using jQTouch, and I'm trying to stay fullscreen at all times; that is, horizontal scrolling is bad. Whenever I click an <input> element, the page scrolls right, showing a black border on the right of the screen and de-centering everything. The inputs are offset from the left of the screen, and they begin somewhere toward the middle of the page.
How can I prevent this scrolling on focus?
I just found the solution to this problem in this post Stop page scrolling from focus
Just add onFocus="window.scrollTo(0, 0);" to your input field and you're done!
(Tried it with <textarea> and <input type="text" /> on the iPad, but I'm pretty sure it'll work on the iPhone too.)
I was afraid the scrolling would be visible as a flicker or something, but fortunately that is not the case!
here is how I solved this on the iPhone (mobile Safari) (I used jQuery)
1) create a global variable that holds the current scroll position, and which is updated every time the user scrolls the viewport
var currentScrollPosition = 0;
$(document).scroll(function(){
currentScrollPosition = $(this).scrollTop();
});
2) bind the focus event to the input field in question. when focused, have the document scroll to the current position
$(".input_selector").focus(function(){
$(document).scrollTop(currentScrollPosition);
});
Ta Da! No annoying "scroll on focus"
One thing to keep in mind...make sure that the input field is ABOVE the keypad, else you will hide the field. That can be easily mitigated by adding an if-clause.
I would recommend using the jQuery.animate() method linked to above, not just the window.scrollTo(0,0), since iOS animates the page offset properties when an input element is focused. Calling window.scrollTo() just once may not work with the timing of this native animation.
For more background, iOS is animating the pageXOffset and pageYOffset properties of window. You can make a conditional check on these properties to respond if the window has shifted:
if(window.pageXOffset != 0 || window.pageYOffset != 0) {
// handle window offset here
}
So, to expand on the aforementioned link, this would be a more complete example:
$('input,select').bind('focus',function(e) {
$('html, body').animate({scrollTop:0,scrollLeft:0}, 'slow');
});
If the focus is being set programmatically, this is what I would do:
Save the window's scollTop value before changing the focus. Then restore the scrollTop to the saved value immediately after setting focus.
If using jQuery:
var savedScrollTop = $(document).scrollTop(); // save scroll position
<code that changes focus goes here>
$(document).scrollTop(savedScrollTop ); // restore it
Try this, i think the problem is the zoom:
<meta name="viewport" content="width=device-width;initial-scale=1.0; maximum-scale=1.0; user-scalable=0;" />
I have a small div box that has a vertical scroll bar and sits within an html page that also has a vertical scroll bar.
My problem is when the user reaches the end of the small DIV box scrolling, the ENTIRE html page that contains the div box then begins to scroll (assuming the user is scrolling via the mouse scroll and NOT by actually clicking the DIV box scroll buttons themselves)
is there a way to prevent the entire html page from scrolling once a user reaches in end of my small DIV box scroll? Any help would be much appreciated! Thank you!
I have tried this (but it cancels scrolling for even the div box):
if (window.addEventListener)
/** DOMMouseScroll is for mozilla. */
window.addEventListener('DOMMouseScroll', handleWheelEvent, false);
/** IE/Opera. */
window.onmousewheel = document.onmousewheel = handleWheelEvent;
function handleWheelEvent(e){
e.preventDefault();
}
I didn't look too much into your code and the problem, but I wanted to throw out a suggestion before I move on :P.
window.addEventListener
and
document.onmousewheel = handleWheelEvent;
are normally good ways to apply what you want to do the ENTIRE document, whereas if you want to apply a specific value (in this case scroll = false) to a specific element, then you need to set the reference to that specific reference (i.e. getElementById() and then it applies only to the element of the document).
Idk - maybe that helps, maybe it doesn't :P good luck.
-J
You would need to modify the handleWheelEvent function and check the srcElement property of the e event and call preventDefault() when it's not scrolling the DIV box. Here's a link with some code examples:
http://www.webdeveloper.com/forum/archive/index.php/t-158824.html
I had a similar problem. Google led me here. Over 1700 views, in 4 years, of an incomplete answer. I figured once I had coded a solution, I'd pop it in a JSFiddle and share it. Better late than never.
Tested on MacOSX / Chrome.
http://jsfiddle.net/mF8Pr/
My problem involved being able to scroll inside a textarea, within a lightbox, and disabling scrolling on the rest of the page beneath the overlay.
bind mouse wheel event to document
when event fires (optional: test to make sure overlay is visible)
check target is obj we want to have scrolling enabled
make sure 0 < obj.scrollTop < (obj.scrollHeight - obj.clientHeight)
check direction of attempted scroll event.originalEvent.deltaY
UP == negative
DOWN == positive
event.preventDefault()
$(document).bind('mousewheel', function(e){
//if($overlay.is(':visible'))
{
if(e.target != null && e.target.type != 'textarea')
{
e.preventDefault();
}
else
{
if(e.originalEvent.deltaY < 0 && e.target.scrollTop == 0)
{
e.preventDefault(); // already at top
}
else if(e.originalEvent.deltaY > 0 && e.target.scrollTop >=
(e.target.scrollHeight - e.target.clientHeight))
{
// must use greater than because sometimes
// the math is wrong by 1px
e.preventDefault(); // already at bottom
}
}
}
});
-Amanda