Dojo dijit tree with checkbox is not keyboard accessible - dojo

I have created a dijit.Tree object where every node is a checkbox. When you select/deselect the parent node, the child nodes get selected/deselected;
when one of the children is deselected, the parent gets deselected; when all the children are selected, the parent gets selected. It works perfectly fine.
However I need it to be keyboard accessible. When I navigate to the tree nodes and press spacebar or Enter, nothing happens.
I tried adding tabindex and aria-role to the checkbox (programmatically), but it did not work.
Here is the fiddle - http://jsfiddle.net/pdabade/pyz9Lcpv/65/
require([
"dojo/_base/window", "dojo/store/Memory",
"dijit/tree/ObjectStoreModel",
"dijit/Tree", "dijit/form/CheckBox", "dojo/dom",
"dojo/domReady!"
], function(win, Memory, ObjectStoreModel, Tree, checkBox, dom) {
// Create test store, adding the getChildren() method required by ObjectStoreModel
var myStore = new Memory({
data: [{
id: 'allDocuments',
name: 'All Documents'
}, {
id: 'inboxDocuments',
name: 'Inbox Documents',
parent: 'allDocuments'
}, {
id: 'outboxDocuments',
name: 'Outbox Documents',
parent: 'allDocuments'
}, {
id: 'draftDocuments',
name: 'Draft Documents',
parent: 'allDocuments'
}, {
id: 'finalDocuments',
name: 'Final Documents',
parent: 'allDocuments'
}],
getChildren: function(object) {
return this.query({
parent: object.id
});
}
});
// Create the model
var myModel = new ObjectStoreModel({
store: myStore,
query: {
id: 'allDocuments'
}
});
// Create the Tree.
var tree = new Tree({
model: myModel,
autoExpand: true,
getIconClass: function(item, opened) {
// console.log('tree getIconClass', item, opened);
// console.log('tree item type', item.id);
},
onClick: function(item, node, event) {
//node._iconClass= "dijitFolderClosed";
//node.iconNode.className = "dijitFolderClosed";
var _this = this;
console.log(item.id);
var id = node.domNode.id,
isNodeSelected = node.checkBox.get('checked');
dojo.query('#' + id + ' .dijitCheckBox').forEach(function(node) {
dijit.getEnclosingWidget(node).set('checked', isNodeSelected);
});
if (item.id != 'allComments') {
if (!isNodeSelected) {
var parent = node.tree.rootNode; // parent node id
//console.log(node);
parent.checkBox.set('checked', false);
} else {
var parent = node.tree.rootNode;
var selected = true;
var i = 0;
dojo.query('#' + parent.id + '.dijitCheckBox').forEach(function(node) {
if (i > 0) {
var isSet = dijit.getEnclosingWidget(node).get('checked');
console.log(isSet);
if (isSet == false) {
selected = false;
}
}
i++;
});
if (selected) {
parent.checkBox.set('checked', true);
}
}
}
//console.log(node.id);
},
_createTreeNode: function(args) {
var tnode = new dijit._TreeNode(args);
tnode.labelNode.innerHTML = args.label;
console.log(args);
var cb = new dijit.form.CheckBox({
"aria-checked": "false",
"aria-describedby": args.label
});
cb.placeAt(tnode.labelNode, "first");
tnode.checkBox = cb;
return tnode;
}
});
tree.placeAt(contentHere);
tree.startup();
tree.checkedItems();
//tree.expandAll();
});
}
Any ideas as to how to make it keyboard accessible?
Thanks!

Looking into the dijit/Tree source I see that it sets the function _onNodePress() as an event handler for keyboard events. You can override it (or add an aspect after it) and handle the key presses you want manually. It takes as argument the tree node and an event object that you can use to check specifically for the space and the enter key.
I forked your jsfiddle with an example: https://jsfiddle.net/pgianna/jjore5sm/1/
_onNodePress: function(/*TreeNode*/ nodeWidget, /*Event*/ e){
// This is the original implementation of _onNodePress:
this.focusNode(nodeWidget);
// This requires "dojo/keys"
if (e.keyCode == keys.ENTER || e.keyCode == keys.SPACE)
{
var cb = nodeWidget.checkBox;
cb.set('checked', !cb.get('checked'));
}
}

Do not add role, aria-checked, nor tabindex to the checkbox. Those are already built into the control, so you are adding risk of breaking it down the road. You can probably also get rid of every role="presentation" as those are on <div>s and <span>s which are presentational by nature. Finally, you need <label> on each block of text that is associated with a checkbox if you want this to be accessible. The aria-describedby is incorrect and is the less good option anyway.
I am getting the error: Uncaught TypeError: tree.checkedItems is not a function (line 159)
You also have a big focus management problem. Put the following in your CSS and you will see that it takes two presses of the Tab key for each single control (if starting at a focused checkbox): :focus {outline:2px solid #f00;}
It looks like you have the containing elements stealing any clicks, meaning the correct element never gets selected. The <span> with classes dijit dijitReset dijitInline dijitCheckBox keeps stealing focus as it toggles its tabindex from -1 to 0, taking itself in and out of the tab order. That may be a factor.
I suggest addressing the script error and then looking at focus management.

With dijit, there's all kinds of stuff going on in the background that might be out of your control. As aardrian said, there's lots of role=presentation and all the aria tags on the <input type='checkbox> are superfluous. dijit is probably (incorrectly) setting all that. An <input type='checkbox> already handles selections and it's role is inherently a checkbox. Those aria properties are for when you're making a custom checkbox out of div/span tags.
There is a native checkbox buried down in the code but it has opacity set to 0 so you can't see it. dijit is probably using it for the checkbox events.
The native checkbox also has data-dojo-attach-event="ondijitclick:_onClick". I'm not sure what that means but anytime I see "click" in an event name, I get suspicious that it might not work with a keyboard.
I tried the example on https://dojotoolkit.org/reference-guide/1.10/dijit/form/CheckBox.html and it works with the keyboard. Hit space will check and uncheck the box. Whether you can see the focus on the checkbox is another issue.
As a side note, it might be nice if your checkbox tree used aria-checked="mixed" for the parent branch. Anytime you have child checkboxes where some are selected and some are not, you can use "mixed" for the parent checkbox to indicate a mixture of selections. Not sure if dijit supports that.

Related

dgrid inside ContentPane - Scroll error

I have a problem with dgrid.... I have an AccordionContainer, and in each ContentPane of it,
I place a dgrid. The problems with the dgrid are:
1- Error with scroll: when scrolling down, in certain moment the scroll "skips" and jumps into the end and there's no way to scroll up and show the first records.
(I have seen in Firebug the error TypeError: grid._rows is null when the scroll fails).
2- Trying to change a value: sounds like no dgrid-datachange event is emitted,
no way to capture the event after editing a value.
I think these errors has to do with having dgrid inside layouts (dgrid inside ContentPane, inside AccordionContainer). I also included the DijitRegistry extension but even with this extensions I can't get
rid of this errors.
I have prepared this fiddle which reproduces the errors:
https://jsfiddle.net/9ax3q9jw/5/
Code:
var grid = new (declare([OnDemandGrid, DijitRegistry,Selection, Selector, Editor]))({
collection: tsStore,
selectionMode: 'none',
columns:
[
{id: 'timestamp', label:'Timestamp', formatter: function (value,rowIndex) {
return value[0];
}},
{id: 'value', label: 'Value',
get: function(value){
return value[1];
},
editor: "dijit/form/TextBox"
}
],
showHeader: true
});
grid.startup();
grid.on('dgrid-datachange',function(event){
alert('Change!');
console.log('Change: ' + JSON.stringify(event));
});
//Add Grid and TextArea to AccordionContainer.
var cp = new ContentPane({
title: tsStore.name,
content: grid
},"accordionContainer");
Any help will be appreciated,
Thanks,
Angel.
There are a couple of issues with this example that may be causing you problems.
Data
The store used in the fiddle is being created with an array of arrays, but stores are intended to work with arrays of objects. This is the root of the scrolling issue you're seeing. One property in each object should uniquely identify that object (the 'id' field). Without entry IDs, the grid can't properly keep track of the entries in your data set. The data array can easily be converted to an object array with each entry having timestamp and value properties, and the store can use timestamp as its ID property (the property it uses to uniquely identify each record).
var records = [];
var data = _globalData[0].data;
var item;
for (var i = 0; i < data.length; i++) {
item = data[i];
records.push({
timestamp: item[0],
value: item[1]
});
}
var tsStore = new declare([Memory, Trackable])({
data: records,
idProperty: 'timestamp',
name: 'Temperature'
});
_t._createTimeSeriesGrids(tsStore);
Setting up the store this way also allows the grid column definitions to be simplified. Using field names instead of IDs will allow the grid to call formatter functions with the corresponding field value for each row object.
columns: [{
field: 'timestamp',
label: 'Timestamp',
formatter: function (value) {
return value;
}
}, {
field: 'value',
label: 'Value',
formatter: function (value) {
return value;
},
editor: "dijit/form/TextBox"
}],
Loading
The fiddle is using declarative widgets and Dojo's automatic parsing functionality to build the page. In this situation the loader callback does not wait for the parser to complete before executing, so the widgets may not have been instantiated when the callback runs.
There are two ways to handle this: dojo/ready or explicit use of the parser.
parseOnLoad: true,
deps: [
...
dojo/ready,
dojo/domReady!
],
callback: function (..., ready) {
ready(function () {
var _t = this;
var _globalData = [];
...
});
}
or
parseOnLoad: false,
deps: [
...
dojo/parser,
dojo/domReady!
],
callback: function (..., parser) {
parser.parse().then(function () {
var _t = this;
var _globalData = [];
...
});
}
Layout
When adding widgets to containers, use Dijit's methods, like addChild and set('content', ...). These typically perform actions other than just adding a widget to the DOM, like starting up child widgets.
var cp = new ContentPane({
title: tsStore.name,
content: grid
});
registry.byId('accordionContainer').addChild(cp);
instead of
var cp = new ContentPane({
title: tsStore.name,
content: grid
}, "accordionContainer");
In the example code a ContentPane isn't even needed since the dgrid inherits from DijitRegistry -- it can be added directly as a child of the AccordionContainer.
This will also call the grid's startup method, so the explicit call in the code isn't needed.
registry.byId('accordionContainer').addChild(grid);
It also often necessary to re-layout the grid's container once the grid has been initially rendered to ensure it's properly sized.
var handle = grid.on('dgrid-refresh-complete', function () {
registry.byId('accordionContainer').resize();
// only need to do this the first time
handle.remove();
});

Dijit.tree extension with radio buttons submitting the wrong value

I've written a class that extends dijit.Tree to include a radio button alongside each node. I'm using it in a form to show a folder tree from which the user can select a folder. Here is the code:
define("my/Tree/RadioButton",
['dojo/_base/declare', 'dijit/Tree', 'dijit/form/RadioButton', 'dojo/dom-construct', 'dojo/_base/connect', 'dojo/on', 'dojo/_base/lang'],
function (declare, Tree, RadioButton, domConstruct, connect, on, lang){
var TreeNode = declare(Tree._TreeNode, {
_radiobutton: null,
postCreate: function(){
this._createRadioButton();
this.inherited(arguments);
},
_createRadioButton: function(){
this._radiobutton = new RadioButton({
name: this.tree.name,
value: this.tree.model.store.getIdentity(this.item) + '',
checked: false
});
domConstruct.place(this._radiobutton.domNode, this.iconNode, 'before');
if (this.tree.model.store.hasAttribute(this.item, 'checked')) {
var attrValue = this.tree.model.store.getValue(this.item, 'checked');
if (attrValue === true) {
this._radiobutton.set('checked', true);
}
}
connect.connect(this._radiobutton, 'onClick', this, function(){
// set any checked items as unchecked in the store
this.tree.model.store.fetch({
query: {checked: true},
onItem: lang.hitch(this.tree.model.store, function(item){
console.log('found checked item ' + this.getValue(item, 'name'));
this.setValue(item, 'checked', false);
})
});
// check the one that was clicked on
var radioValue = this._radiobutton.get('value');
this.tree.model.store.setValue(this.item, 'checked', true);
});
}
});
return declare(Tree, {
_createTreeNode: function(/*Object*/ args){
return new TreeNode(args);
}
});
});
The issue is that when the form is submitted, the value that is submitted is always the value of the first radio button that was selected, even if other radio buttons are subsequently clicked on.
I can see by inspecting the dom that the value attribute for the checked radio button has the correct value. But what gets submitted is always the initially selected value.
I have a similar class that uses the checkbox widget instead and that one works fine.
Edit based on some feedback I created an even simpler version of this class that doesn't track the checked state using attribute in the store:
define("my/Tree/RadioButton",
['dojo/_base/declare', 'dijit/Tree', 'dijit/form/RadioButton', 'dojo/dom-construct'],
function (declare, Tree, RadioButton, domConstruct){
var TreeNode = declare(Tree._TreeNode, {
_radiobutton: null,
postCreate: function(){
this._createRadioButton();
this.inherited(arguments);
},
_createRadioButton: function(){
this._radiobutton = new RadioButton({
name: this.tree.name,
value: this.tree.model.store.getIdentity(this.item) + '',
checked: false
});
domConstruct.place(this._radiobutton.domNode, this.iconNode, 'before');
}
});
return declare(Tree, {
_createTreeNode: function(/*Object*/ args){
return new TreeNode(args);
}
});
});
but even this still has the same issue - whichever radio button the user clicks on first is the value that will be submitted, regardless of what other buttons are subsequently clicked.
I managed to workaround this issue by hooking on to the onchange event for the radio buttons. The hook explicitly sets checked to false on the unchecked radio button, which seems to fix the problem. I'm unsure why this is required though.
I have this exact same problem. It used to work in older Dojos. Specifically, ALL of the radioButtons incorrectly return true on "dijit.byId("whatever").checked" during the onClicked function. When checked manually after the onClicked function finishes using FireBug console, the above property returns the correct values. I think it is a bug, and I only worked around it by having a different onClicked function for each button, like so:
<form id="locateForm">
<label for="locate">Locate:</label><br />
<input type="radio" dojoType="dijit.form.RadioButton" name="locate" id="locateAddress" value="Address" checked="checked" onClick="enableLocate1();" />
<label for="locateAddress">Address</label>
<input type="radio" dojoType="dijit.form.RadioButton" name="locate" id="locatePlace" value="Place" onClick="enableLocate2();" />
<label for="locatePlace">Place</label>
</form>

making Ext.Action's text property dependent on another field's value

I have a grid within a window. The grid has 3 Actions: Edit, Delete and Disable.I was wondering if it is possible to make the text of the Disable Action (which is currently 'Disable/Enable') to be dependent on the Current Status of the record selected. So say the user selects a record whose Current Status is Enabled, then the action's text should be 'Disable'. If, however, the user selects a record whose status is Disabled, then the action's text should be 'Enable'. Is it possible to do this when using Action? Or do I need to use a button instead of Action?
I am assuming your action button is in a toolbar that is docked to the top of your grid panel. The only tricky thing is getting a reference to the grid (without hardcoding it). The grid's 'select' event only gives you a reference to the rowmodel used.
/* Set a action attribute on the Ext.Action so we can find it */
var action = new Ext.Action({
text: 'Do something',
handler: function(){
Ext.Msg.alert('Click', 'You did something.');
},
iconCls: 'do-something',
itemId: 'myAction',
action: 'myAction' // I don't like itemId's personally :)
});
/* In the Controller */
init: function() {
this.control({
'mygrid': {
select: this.onRecordSelect
}
});
},
onRecordSelect: function(rowModel, record) {
var grid = rowModel.views[0].ownerCt);
var action = grid.getDockedItems('toolbar[dock="top"]')[0].down('button[action="myAction"]');
var enabled = (record.get('CurrentStatus') == "Enabled");
action.setText(enabled ? 'Disable' : 'Enable');
action.setIconCls(enabled ? 'myDisableCls' : 'myEnableCls');
}
/* in SASS */
.myDisableCls{
background-image:url(#{$icon_path}/checkbox.png) !important;
}
.myEnableCls {
background-image:url(#{$icon_path}/checkbox_ticked.png) !important;
}
Good luck!
I solved the problem in another way. This is my code:
grid.getSelectionModel().on({
selectionchange: function(sm, selections) {
if (selections.length > 0) {
Edit.enable();
Delete.enable();
if(selections[0].data.CurrentStatus == "Disabled"){
Disable.setText("Enable");
Disable.enable();
}else{
Disable.setText("Disable");
Disable.enable();
}
} else {
Edit.disable();
Delete.disable();
Disable.disable();
}
}
});

onfocus event not firing on dojo dijit FilteringSelect

I have a page with 7 dijit FilteringSelect widgets that I want to connect to stores fetched from the server if the user clicks on the widget or the widget otherwise gains focus. I want the event to fire one time.
If I add onfocus="loadDropDown(this)" to my markup, it executes every time the widget gains focus, as you would expect.
I'm trying to use dojo to fire the event one time using on.once(). The function to use dojo event handling is running but the event handler function never gets called when a widget gains focus.
Any pointers?
This is my markup
<select data-dojo-type="dijit.form.FilteringSelect"
type="text" intermediateChanges="false"
data-dojo-props="required:false, pageSize:20, placeholder: '---'"
scrollOnFocus="true" name="CJ1lxA" style="width: 40em;"
id="searchAgency">
</select>
This is to regester the events
function registerDDLoad(){
require(["dojo/on", "dijit", "dojo/ready"], function(on, dijit, ready){
ready(function(){
var dropDown = dijit.byId("searchAgency");
on.once(dropDown, "onfocus", function() {
loadDropDown(dropDown);
});
dropDown = dijit.byId("searchLocation");
on.once(dropDown, "onfocus", function() {
loadDropDown(dropDown);
});
dropDown = dijit.byId("searchCounty");
on.once(dropDown, "onfocus", function() {
loadDropDown(dropDown);
});
dropDown = dijit.byId("searchRep");
on.once(dropDown, "onfocus", function() {
loadDropDown(dropDown);
});
dropDown = dijit.byId("searchSenate");
on.once(dropDown, "onfocus", function() {
loadDropDown(dropDown);
});
dropDown = dijit.byId("searchStatus");
on.once(dropDown, "onfocus", function() {
loadDropDown(dropDown);
});
dropDown = dijit.byId("searchAE");
on.once(dropDown, "onfocus", function() {
loadDropDown(dropDown);
});
});
});
}
registerDDLoad();
The dojo event class, dojo/on expects events to be specified without the 'on':
onFocus = focus
onClick = click
onMouseOut = mouseout
...
I think changing that should fix your problem. I've copied your code into test area on jsFiddle, so you can play around with it.
NB: Since you are re-using the dropdown variable, it will always equal the last filteringSelect (id=searchAE) and never the earlier ones.

Dojo/Dijit TitlePane

How do you make a titlePane's height dynamic so that if content is added to the pane after the page has loaded the TitlePane will expand?
It looks like the rich content editor being an iframe that is loaded asynchronously confuses the initial layout.
As #missingno mentioned, the resize function is what you want to look at.
If you execute the following function on your page, you can see that it does correctly resize everything:
//iterate through all widgets
dijit.registry.forEach(function(widget){
//if widget has a resize function, call it
if(widget.resize){
widget.resize()
}
});
The above function iterates through all widgets and resizes all of them. This is probably unneccessary. I think you would only need to call it on each of your layout-related widgets, after the dijit.Editor is initialized.
The easiest way to do this on the actual page would probably to add it to your addOnLoad function. For exampe:
dojo.addOnLoad(function() {
dijit.byId("ContentLetterTemplate").set("href","index2.html");
//perform resize on widgets after they are created and parsed.
dijit.registry.forEach(function(widget){
//if widget has a resize function, call it
if(widget.resize){
widget.resize()
}
});
});
EDIT: Another possible fix to the problem is setting the doLayout property on your Content Panes to false. By default all ContentPane's (including subclasses such as TitlePane and dojox.layout.ContentPane) have this property set to true. This means that the size of the ContentPane is predetermined and static. By setting the doLayout property to false, the size of the ContentPanes will grow organically as the content becomes larger or smaller.
Layout widgets have a .resize() method that you can call to trigger a recalculation. Most of the time you don't need to call it yourself (as shown in the examples in the comments) but in some situations you have no choice.
I've made an example how to load data after the pane is open and build content of pane.
What bothers me is after creating grid, I have to first put it into DOM, and after that into title pane, otherwise title pane won't get proper height. There should be cleaner way to do this.
Check it out: http://jsfiddle.net/keemor/T46tt/2/
dojo.require("dijit.TitlePane");
dojo.require("dojo.store.Memory");
dojo.require("dojo.data.ObjectStore");
dojo.require("dojox.grid.DataGrid");
dojo.ready(function() {
var pane = new dijit.TitlePane({
title: 'Dynamic title pane',
open: false,
toggle: function() {
var self = this;
self.inherited('toggle', arguments);
self._setContent(self.onDownloadStart(), true);
if (!self.open) {
return;
}
var xhr = dojo.xhrGet({
url: '/echo/json/',
load: function(r) {
var someData = [{
id: 1,
name: "One"},
{
id: 2,
name: "Two"}];
var store = dojo.data.ObjectStore({
objectStore: new dojo.store.Memory({
data: someData
})
});
var grid = new dojox.grid.DataGrid({
store: store,
structure: [{
name: "Name",
field: "name",
width: "200px"}],
autoHeight: true
});
//After inserting grid anywhere on page it gets height
//Without this line title pane doesn't resize after inserting grid
dojo.place(grid.domNode, dojo.body());
grid.startup();
self.set('content', grid.domNode);
}
});
}
});
dojo.place(pane.domNode, dojo.body());
pane.toggle();
});​
My solution is to move innerWidget.startup() into the after advice to "toggle".
titlePane.aspect = aspect.after(titlePane, 'toggle', function () {
if (titlePane.open) {
titlePane.grid.startup();
titlePane.aspect.remove();
}
});
See the dojo/aspect reference documentation for more information.