When a web page is loaded, screen readers (like the one that comes with OS X, or JAWS on Windows) will read the content of the whole page. But say your page is dynamic, and as users performing an action, new content gets added to the page. For the sake of simplicity, say you display a message somewhere in a <span>. How can you get the screen reader to read that new message?
The WAI-ARIA specification defines several ways by which screen readers can "watch" a DOM element. The best supported method is the aria-live attribute. It has modes off, polite,assertive and rude. The higher the level of assertiveness, the more likely it is to interrupt what is currently being spoken by the screen reader.
The following has been tested with NVDA under Firefox 3 and Firefox 4.0b9:
<!DOCTYPE html>
<html>
<head>
<script src="js/jquery-1.4.2.min.js"></script>
</head>
<body>
<button onclick="$('#statusbar').html(new Date().toString())">Update</button>
<div id="statusbar" aria-live="assertive"></div>
</body>
The same thing can be accomplished with WAI-ARIA roles role="status" and role="alert". I have had reports of incompatibility, but have not been able to reproduce them.
<div id="statusbar" role="status">...</div>
Here is an adapted real world example -- this up-level markup has already been converted from an unordered list with links into a select menu via JS. The real code is a lot more complex and obviously could not be included in its entirety, so remember this will have to be rethought for production use. For the select menu to be made keyboard accessible, we registered the keypress & onchange events and fired the AJAX call when users tabbed off of the list (beware of browser differences in timing of the onchange event). This was a serious PITA to make accessible, but it IS possible.
// HTML
<!-- select element with content URL -->
<label for="select_element">State</label>
<select id="select_element">
<option value="#URL_TO_CONTENT_PAGE#" rel="alabama">Alabama</option>
</select>
<p id="loading_element">Content Loading</p>
<!-- AJAX content loads into this container -->
<div id="results_container"></div>
// JAVASCRIPT (abstracted from a Prototype class, DO NOT use as-is)
var selectMenu = $('select_element');
var loadingElement = $('loading_element');
var resultsContainer = $('results_container');
// listen for keypress event (omitted other listeners and support test logic)
this.selectMenu.addEventListener('keypress', this.__keyPressDetector, false);
/* event callbacks */
// Keypress listener
__keyPressDetector:function(e){
// if we are arrowing through the select, enable the loading element
if(e.keyCode === 40 || e.keyCode === 38){
if(e.target.id === 'select_element'){
this.loadingElement.setAttribute('tabIndex','0');
}
}
// if we tab off of the select, send focus to the loading element
// while it is fetching data
else if(e.keyCode === 9){
if(targ.id === 'select_element' && targ.options[targ.selectedIndex].value !== ''){
this.__changeStateDetector(e);
this.loadingElement.focus();
}
}
}
// content changer (also used for clicks)
__changeStateDetector:function(e){
// only execute if there is a state change
if(this.selectedState !== e.target.options[e.target.selectedIndex].rel){
// get state name and file path
var stateName = e.target.options[e.target.selectedIndex].rel;
var stateFile = e.target.options[e.target.selectedIndex].value;
// get the state file
this.getStateFile(stateFile);
this.selectedState = stateName;
}
}
getStateFile:function(stateFile){
new Ajax.Request(stateFile, {
method: 'get',
onSuccess:function(transport){
// insert markup into container
var markup = transport.responseText;
// NOTE: select which part of the fetched page you want to insert,
// this code was written to grab the whole page and sort later
this.resultsContainer.update(markup);
var timeout = setTimeout(function(){
// focus on new content
this.resultsContainer.focus();
}.bind(this), 150);
}.bind(this)
});
}
It really depends whether you are just adding some messages or replacing large parts of the page.
Messages
There are Aria Live Regions, which announce any change to their contents. This is very useful for status messages and sometimes even used with visually hidden live regions to only address screen reader users.
<button onclick="document.querySelector('#statusbar').innerHTML = new Date().toString()">Update</button>
<div id="statusbar" aria-live="assertive"></div>
The aria-live attribute establishes a live region and its value is a politeness setting, which regulates how likely it is the change will interrupt what is currently being spoken by the screen reader.
Another classic example is inline validation of form fields, where the alert role, a live region, is used to immediately announce the error message to the user:
<label>Day of the week we hate
<input type="text" aria-describedby="error">
</label>
<div role="alert" id="error" hidden>only Monday is permitted</div>
Large Parts of Content
When JavaScript is changing large parts of the site, like in single page applications, putting everything inside a live region would be overkill and actually very annoying.
To let the user know that content changed after activating a trigger, two approaches exist:
A new state of the trigger is announced, implying that the user can simply continue reading to find the new content
Focus is put either onto the element who’s content changed, or on the first interactive element inside
Simply read on
The first case would be applied if the role of the trigger (or other status information) makes it clear that a content change will happen, so it’s expected.
A classic example is the accordion. It has aria-expanded state, which communicates whether its contents are currently visible or not. If they are, the user will simply continue reading, because contents should follow immediately after.
toggleAccordion = e => {
const target = document.getElementById(e.currentTarget.getAttribute('aria-controls'));
e.currentTarget.setAttribute('aria-expanded', ! target.toggleAttribute('hidden'));
}
<!-- soon to be replaced by <details> and <summary> -->
<button aria-expanded="false" aria-controls="accordion-content" onclick="toggleAccordion(event)">2.1 First Rule of ARIA Use</button>
<blockquote id="accordion-content" hidden>
<p>If you can use a native HTML element [HTML51] or attribute with the semantics and behavior you require already built in, instead of re-purposing an element and adding an ARIA role, state or property to make it accessible, then do so. […]</p>
</blockquote>
Put focus on the new content
In the second case focus is set programmatically elsewhere, so that element will be announced. This is particularly helpful if it’s parent elements have grouping roles, so their names will be announced as well, as in the case of a modal dialog.
Another example would be a single page application’s navigation, where the single navigation items are still navigated by means of tab.
To be able to programmatically focus a non-interactive element, but not manually, tabindex="-1" is necessary. Focussing the headline is a best practice.
/* some sort of SPA router */
document.querySelectorAll('nav a').forEach(a => a.addEventListener('click', e => {
// hide all visible contents
document.querySelectorAll('main > :not([hidden])').forEach(c => c.hidden = true);
document.querySelectorAll('[aria-current]').forEach(c => c.removeAttribute('aria-current'));
// show selected content
const content = document.querySelector(e.currentTarget.getAttribute('href'));
content.hidden = false;
content.querySelector('h1').focus();
e.currentTarget.setAttribute('aria-current', 'page');
}));
a[aria-current] { font-weight: bold }
<nav>
<ul>
<li>Page 1</li>
<li>Page 2</li>
</ul>
</nav>
<main>
<div id="page-1">
<h1 tabindex="-1">Page 1</h1>
<p>Many lines of content to follow</p>
</div>
<div id="page-2" hidden>
<h1 tabindex="-1">Page 2</h1>
<p>Many lines of content to follow</p>
</div>
</main>
Related
I am trying to figure out how to click a button on the D365 ribbon. The button will refresh the page and i am going this route because ultimately i want to refresh all of the elements on the page.
I have tried accessing via query selector with no luck
document.querySelector("#rr_jobprofile\\|NoRelationship\\|Form\\|Mscrm\\.Modern\\.refreshCommand72 > button").click();
html for the button
<button aria-label="Refresh" aria-hidden="true" title="Refresh" tabindex="-1" data-id="rr_jobprofile|NoRelationship|Form|Mscrm.Form.rr_jobprofile.RefreshModernButton" data-lp-id="Form:rr_jobprofile-rr_jobprofile|NoRelationship|Form|Mscrm.Form.rr_jobprofile.RefreshModernButton" type="button" class="pa-ak pa-kx pa-go pa-ep pa-aj pa-om pa-at pa-sx pa-sy flexbox"><span class="pa-az pa-ah pa-a pa-hh "><span class="pa-ho pa-hj pa-st pa-cd pa-bd pa-a pa-at "><img src="/uclient/resources/images/Refresh.svg?v=1.4.2043-2012.2" alt="" title="" class="pa-oh pa-cg pa-bd pa-cc "></span><span aria-label="Refresh" tabindex="-1" class="pa-hj pa-bd pa-st pa-v pa-e pa-cm pa-oz pa-cl ">Refresh</span></span></button>
Really really really recommend against manipulating or navigating the DOM. There are methods in the formContext.Controls collection to refresh any control on the page that requires it, or the page itself. Refreshing an HTML web resource is a little less obvious, but I've had good success using the getSrc() and setSrc() functions of the control. This function (not mine I got it on some blog years ago and added it to my toolbox) works very well, and will work both on the form (e.g. on load or on change) and from the ribbon.
function refreshWebResource(executionContext, WebRrscName) {
var _crmForm = executionContext.getFormContext();
var webResource = _crmForm.getControl(WebRrscName);
if (webResource != null) {
var webResourceSrc = webResource.getSrc();
webResource.setSrc(null);
webResource.setSrc(webResourceSrc);
}
}
Code's original source: https://community.dynamics.com/crm/f/microsoft-dynamics-crm-forum/232589/refreshing-the-web-resource-in-dynamics-365
Try
document.querySelector('[data-id="rr_jobprofile|NoRelationship|Form|Mscrm.Form.rr_jobprofile.RefreshModernButton"]').click()
Selectors for data attributes use attribute selectors syntax instead of starting with a tag name or characters # or .. For example
button[data-id=xxxx] selects a button whose data-id attribute value is exactly xxxx.
button[data-id|=xxxx] selects a button whose data-id attribute value starts with xxxx`.
`
I have page with two tabs; one of them randomly will be active and its date will be loaded by default. The second tab, will be inactive until the user will click it then it will load the content using Ajax. (never been loaded yet)
All what I need to do is to fire this code in both tabs:
$('#listings').attr('class', 'mobile row');
But I fail to make it run in the inactive tab as the JQuery always stop before Ajax complete loading.
This is the tab HTML code:
<ul class="tabs tabs-hash">
<li class="" id="tab_listings" lang="listings">
Property
</li>
<li class="active" id="tab_automobile" lang="automobile">
Vehicles
</li>
</ul>
And this is the content will show when each tab will clicked:
Tab#1 (Active)
<div class="tab_area" id="area_automobile" style="display: block;">
<section id="listings" class="mobile row">
some content here
</section>
</div>
Tab#2 (Inactive)
<div class="tab_area hide" id="area_listings" style="display: none;">
<span class="text-notice">**Loading...** </span><!-- This span is loading content using Ajax and JQuery finish before its complete and this content will have new section with the target class need to be changed by Jquery -->
</div>
Once Inactive tab become Active and Ajax complete loading
this section will appear and the class="mobile row" need to be changed
<section id="listings" class="mobile row">
some content here
</section>
I tried to search Stack Overflow but all the post I found did not solve my problem.
Is it possible someone can explain a way to achieve target?
After deep search, I found the solution did what I want in mozilla.org
Which explained there in details, but the short summary its the way to instantiating new DOM mutation observers with compatibility with all new major browser.
So, If the DOM class, id, attributes, etc... will appear in the future MutationObserver will detected it and apply the rule (we can stop it any time) and its support:
child elements target's
Attributes target's
Data target's descendants
Attribute value
Data before the mutation (Past/Old data)
Local names (without namespace)
The code I used to complete the above target as the following:
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (!mutation.addedNodes) return
for (var i = 0; i < mutation.addedNodes.length; i++) {
$('section#listings').attr('class', 'mobile row');
var node = mutation.addedNodes[i]
}
})
})
observer.observe(document.body, {
childList: true
, subtree: true
, attributes: false
, characterData: false
})
I hope this can help others and for me this is the best solution for now unless some one else give me extra help and better advise.
NOTE:
The above code, is taken from one post of Stackoverflow posts, unfortunately,
By mistake I closed the tab of my browser, and I coudl't get the post back even by search on Google. I wish if I could gave credit to the adviser with this code, and I should say thank you for both, StackOverFlow, and The code maker.
Edited:
I change the Class attr. code to be like this:
$('section.list').attr('class', 'mobile row');
As my page include 3 section with the same #ID and this cause the JQuery to stop once he found ID match but not with the Class(s)
Tariq
I'm using ng2-modal to create a modal dialog in Angular 2. I am unable to get an element that has a condition on it (#myModalScrollPanel).
Template (modal.html):
<modal #myModal title="{{title}}"
cancelButtonLabel="Close"
submitButtonLabel="Ok"
modalClass="#modal-md"
[hideCloseButton]="false"
[closeOnEscape]="false"
[closeOnOutsideClick]="false"
>
<modal-header></modal-header>
<modal-content>
<div class="row">
<div *ngIf="logOutput" class="col-md-12">
<div id="myModalScrollPanel" #myModalScrollPanel class="panel-body fixed-panel">
<p *ngFor="let entry of logOutput.entries">{{entry.log}}</p>
</div>
</div>
</div>
</modal-content>
<modal-footer></modal-footer>
</modal>
In my component I get View Children (modal.component.ts):
#ViewChild('myModal') myModalModal;
#ViewChild('myModalScrollPanel') myModalScrollPanel;
After a users clicks a button, I show the modal and get log text from the server, I call updateScrollPosition() once I have updated logOutput (modal.component.ts):
updateScrollPosition() {
console.log("modal:");
console.log(this.myModalModal);
console.log("log scroll panel:");
console.log(this.myModalScrollPanel);
if(this.myModalScrollPanel){
console.log("Scrolling the panel down");
this.myModalScrollPanel.nativeElement.scrollTop = this.myModalScrollPanel.nativeElement.scrollHeight;
} else {
console.log("No log scroll panel.");
}
}
The console shows a valid object for "modal" and 'undefined' for log scroll panel.
I also tried calling that method from ngAfterViewInit(), but it only gets called when the parent component is loaded, and not the actual modal being shown, so the if statement results in false and element doesn't exist yet. If I remove/move the if statement, so that the element exists I can then access that object, but the changes I make do not take any effect, although that might be a whole different issue (most likely to do with change detection).
In the end, I just want to scroll to the bottom of the panel. Something like this (which works in the browser console):
var scrollPanel = document.getElementById('myModalScrollPanel');
scrollPanel.scrollTop = scrollPanel.scrollHeight;
Thanks in advance!
So to answer my own question, I had to use #ViewChildren instead of #ViewChild.
#ViewChildren annotation will assign a QueryList object to your variable, giving you access to the elements you require (I use .toArray() to get an array of elements, which in this case will be just one).
Speaking of the change detection problem I had, thanks to this answer, using the ngAfterViewChecked() method I was able to implement the scroll down I needed.
Something like this:
<!-- HTML on page -->
<!-- hide the following div by class-->
<div id="bed" class="blankets">
We sell blankets
</div>
<!-- hide the following div by ID -->
<div id="animal" class="frogs">
We sell frogs
</div>
<!-- this is the only div I want visible -->
<div id="house" class="speakers">
We sell houses and speakers
</div>
<!-- hide the following div by class -->
<div id="car" class="suvs">
We sell cars
</div>
JavaScript to inject on page via UserScript and compatible with GreaseMonkey, Safari, Google Chrome and Opera.
var div = document.getElementById("animal");
if (div) {
div.parentNode.removeChild(div);
};
var cur_columns = document.getElementsByClassName('blankets');
var cur_columns = document.getElementsByClassName('suvs');
if (div) {
div.parentNode.removeChild(div);
};
I only want to see the content in the middle of the page, while if the top ever changes server side value, but bottom stays the same, the bottom will still hide, now I jut have to update the code to the value.
What I see on the internet are only ways to execute ONE variable, IF the matches ALL, OR (one of the other). It HAS to match a whole rule to a single point.
I would like this to work on the browsers mentioned in the comment please.
I also don't know how to use comments. I've use // and /* /, is / *\ the way to close a comment?
UPDATE
Now how do I add another line for the var on Class so that I can hide a 4th div?
It's very difficult to work out what you want to achieve here, but I can explain what your current script does:
// The variable 'div' refers to <div id="frogs">
var div = document.getElementById("frogs");
// If 'div' exists, remove it
if (div) {
div.parentNode.removeChild(div);
};
// The variable 'cur_columns' refers to all elements with class="speaker"
var cur_columns = document.getElementsByClassName('speakers');
// ...and now it refers to all elements with class="suvs"!
var cur_columns = document.getElementsByClassName('suvs');
// If 'div' exists, remove it (again)
if (div) {
div.parentNode.removeChild(div);
};
I am creating a mobile app (Phonegap/Cordova 1.5.0, JQM 1.1.0) and testing on iOS 5.1. I have a list of items that the user "owns" or wants to own. Throughout the app, the user can edit their list by adding and removing items. Whenever items are added or removed, the list updates, and it is displaying fine, with all of the JQuery CSS intact except the corners are no longer rounded (I'm thinking because data-inset is getting set to "false").
Here is my html for the list-headers:
<div data-role="page" id="profile">
<div data-role="header" data-position="fixed">
<...>
</div><!-- /header -->
<div data-role="content" data-theme="a">
<...>
<ul id="user-wants-list" data-role="listview" data-inset="true" data-theme="d" data-dividertheme="d" >
</ul> <!--/Wants list-->
</br>
<ul id="user-haves-list" data-role="listview" data-inset="true" data-theme="d" data-dividertheme="d" >
</ul> <!--/Has list-->
</br></br>
</div> <!--/content-->
</div> <!--/Profile-->
And here is the Javascript where I remove the old list and dynamically add the new one (the parameter 'haves' is an array of objects):
function displayHaves(haves){
var parent = document.getElementById('user-haves-list');
removeChildrenFromNode(parent);
parent.setAttribute('data-inset','true');
$(parent).listview("refresh");
var listdiv = document.createElement('li');
listdiv.setAttribute('id','user-haves-list-divider');
listdiv.setAttribute('data-role','list-divider');
listdiv.innerHTML = "I Have (" + haves.length + ")";
parent.appendChild(listdiv);
//create dynamic list
for(i=0;i<haves.length;i++){
var sellListing = haves[i].listing;
var userInfo = haves[i].user;
var itemData = haves[i].item;
//create each list item
var listItem = document.createElement('li');
listItem.setAttribute('id','user-haves-list-item-'+i);
parent.appendChild(listItem);
var link = document.createElement('a');
link.setAttribute('id','user-haves-link-' + i);
new FastButton(link, function(listing) {
return function() { displaySellListingPage(listing); }
}(sellListing));
listItem.appendChild(link);
var link = document.getElementById('user-haves-link-' + i);
var pic = document.createElement('img');
pic.setAttribute('src',itemData.pictureURL);
pic.setAttribute('width','80px');
pic.setAttribute('height','100px');
pic.setAttribute('style','padding-left: 10px');
link.appendChild(pic);
var list = document.getElementById('user-haves-list');
$(list).listview("refresh");
}
}
and my function removeChildrenFromNode(parent) is as follows:
function removeChildrenFromNode(node){
while (node.hasChildNodes()){
node.removeChild(node.firstChild);
}
}
So my question is, why does the listview lose the data-inset attribute?
Or, equally valid: is there another way I could/should be achieving corner rounding besides "data-inset='true'"?
Here are things I have tried:
using .trigger("create") on both the listview and the page
adding the listview with explicit styling each time by using $("#page-ID").append(...)
I read another post on StackOverflow that said that JQM creates some inner elements when you create an item (this post had to do with dynamic buttons not being the right size), and that there are some classes (like .ui-btn) that you can access (that may be losing styling when I remove the children from the node?), but I was unable to make any headway in that direction.
Thanks in advance for the help!
I figured out the answer to my question, but not a solution (yet).
$(list).listview('refresh') was getting called on some elements before they had been put on the page, so it was essentially being called on nothing (or another way to think about it is that each list item being appended happens after the refresh call, so it overrides some of the visual styling).
I know the problem has to do with asynchronous loading in javascript. Essentially, the .listview('refresh) executes before the earlier code, which creates the elements but takes longer to execute. I understand the reasoning behind the design, but is there some way to get around this in this case?
I am thinking some conditional that I could set, like:
var doneLoading = false;
//Then when finished set doneLoading to 'true'
if(doneLoading) $(list).listview('refresh');
but if the refresh gets called first, I figure that doneLoading will just evaluate to false and then not execute once the list is actually done loading.
Is there any kind of onComplete callback I can use, or a way to make it happen synchronously?
Try calling listview(refresh) after updating the HTML.