Skip to content

Commit

Permalink
Fix primefaces#11850: SelectCheckBoxMenu ARIA accessibility updates
Browse files Browse the repository at this point in the history
  • Loading branch information
melloware committed May 25, 2024
1 parent ed7c173 commit 5863954
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 97 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Stream;

Expand All @@ -43,7 +44,11 @@
import org.primefaces.component.column.Column;
import org.primefaces.expression.SearchExpressionUtils;
import org.primefaces.renderkit.SelectManyRenderer;
import org.primefaces.util.*;
import org.primefaces.util.ComponentUtils;
import org.primefaces.util.Constants;
import org.primefaces.util.FacetUtils;
import org.primefaces.util.HTML;
import org.primefaces.util.WidgetBuilder;

public class SelectCheckboxMenuRenderer extends SelectManyRenderer {

Expand Down Expand Up @@ -474,7 +479,7 @@ protected void encodePanelContent(FacesContext context, SelectCheckboxMenu menu,
});
}
else {
// Rendering was moved to the client - see renderPanelContentFromHiddenSelect as part of forms.selectcheckboxmenu.js
// Rendering was moved to the client - see renderItems as part of forms.selectcheckboxmenu.js
}
}

Expand Down Expand Up @@ -722,20 +727,17 @@ protected String getSubmitParam(FacesContext context, UISelectMany selectMany) {
protected void encodeKeyboardTarget(FacesContext context, SelectCheckboxMenu menu) throws IOException {
ResponseWriter writer = context.getResponseWriter();
String inputId = menu.getClientId(context) + "_focus";
String tabindex = menu.getTabindex();
String tabindex = menu.isDisabled() ? "-1" : Objects.toString(menu.getTabindex(), "0");

writer.startElement("div", null);
writer.writeAttribute("class", "ui-helper-hidden-accessible", null);
writer.startElement("input", menu);
writer.writeAttribute("id", inputId, null);
writer.writeAttribute("name", inputId, null);
writer.writeAttribute("type", "text", null);
writer.writeAttribute("readonly", "readonly", null);
writer.writeAttribute(HTML.ARIA_ROLE, HTML.ARIA_ROLE_COMBOBOX, null);
writer.writeAttribute(HTML.ARIA_HIDDEN, "true", null);
if (tabindex != null) {
writer.writeAttribute("tabindex", tabindex, null);
}
writer.writeAttribute("tabindex", tabindex, null);
writer.endElement("input");
writer.endElement("div");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,10 @@ PrimeFaces.widget.SelectCheckboxMenu = PrimeFaces.widget.BaseWidget.extend({
item.attr('data-item-value', input.val());
}

item.find('> .ui-chkbox > .ui-helper-hidden-accessible > input').prop('checked', checked).attr('id', uuid);
item.find('> .ui-chkbox > .ui-helper-hidden-accessible > input')
.prop('checked', checked)
.attr('class', 'ui-selectcheckboxmenu-item-input')
.attr('id', uuid);
$this.itemContainer.attr('role', 'group');

$this.itemContainer.append(item);
Expand Down Expand Up @@ -525,44 +528,109 @@ PrimeFaces.widget.SelectCheckboxMenu = PrimeFaces.widget.BaseWidget.extend({
if (!$this.isLoaded()) {
$this._renderPanel();
}

var isPopupVisible = $this.panel.is(":visible");
var isPopupHidden = !isPopupVisible;

switch (e.code) {
case 'Enter':
case 'NumpadEnter':
case 'Space':
$this.togglePanel();
if ($this.panel.is(":hidden")) {
e.stopPropagation(); // GitHub #8340
if (isPopupHidden) {
$this.show();
e.stopPropagation(); // GitHub #8340
}
e.preventDefault();
break;

case 'ArrowDown':
if (e.altKey) {
$this.togglePanel();
if (isPopupHidden) {
$this.show();
}

e.preventDefault();
break;

case 'Tab':
if ($this.panel.is(':visible')) {
if (!$this.cfg.showHeader) {
//".ui-chkbox" is a grandchild when columns are used!
$this.items.filter(':not(.ui-state-disabled):first').find('div.ui-chkbox > div.ui-helper-hidden-accessible > input').trigger('focus');
}
else {
$this.togglerCheckboxInput.trigger('focus');
}
e.preventDefault();

case 'ArrowUp':
if (isPopupHidden) {
$this.show('.ui-selectcheckboxmenu-item-input:focusable:last');
}

else if (e.altKey) {
$this.hide();
}

e.preventDefault();
break;
case 'Escape':
// Dismisses the popup if it is visible. Optionally, if the popup is hidden before Escape is pressed, clears the combobox.
if (isPopupVisible) {
$this.hide();
}
else {
$this.resetValue(false);
}

break;
};
});
},

/**
* Sets up the keyboard event listeners for the given checkbox options.
* @private
* @param {JQuery} items Checkbox options for which to add the event listeners.
*/
bindCheckboxKeyEvents: function(items) {
var $this = this;

items.on('focus.selectCheckboxMenu', function(e) {
var input = $(this),
box = input.parent().next();

box.addClass('ui-state-focus');

PrimeFaces.scrollInView($this.itemContainerWrapper, box);
}).on('blur.selectCheckboxMenu', function(e) {
var input = $(this),
box = input.parent().next();

box.removeClass('ui-state-focus');
}).on('keydown.selectCheckboxMenu', function(e) {
var currentCheckbox = $(this);
var index = 0;
switch (e.code) {
case 'Enter':
case 'NumpadEnter':
$this.toggleItem(currentCheckbox.parent().next());
e.preventDefault();
break;
case 'PageUp':
case 'Home':
items.first().trigger('focus');
e.preventDefault();
break;
case 'PageDown':
case 'End':
items.last().trigger('focus');
e.preventDefault();
break;
case 'ArrowUp':
index = items.index(currentCheckbox) - 1;
if (index >= 0) {
items.eq(index).trigger('focus');
}
e.preventDefault();
break;
case 'ArrowDown':
index = items.index(currentCheckbox) + 1;
if (index < items.length) {
items.eq(index).trigger('focus');
}
e.preventDefault();
break;
case 'Escape':
$this.hide();
e.preventDefault();
break;
};
}
});
},

Expand All @@ -588,37 +656,40 @@ PrimeFaces.widget.SelectCheckboxMenu = PrimeFaces.widget.BaseWidget.extend({
if (this.cfg.showHeader) {
this.closer.on('focus.selectCheckboxMenu', function(e) {
$this.closer.addClass('ui-state-focus');
})
.on('blur.selectCheckboxMenu', function(e) {
$this.closer.removeClass('ui-state-focus');
})
.on('keydown.selectCheckboxMenu', function(e) {
switch (e.key) {
case 'Enter':
case ' ':
$this.hide();
}).on('blur.selectCheckboxMenu', function(e) {
$this.closer.removeClass('ui-state-focus');
}).on('keydown.selectCheckboxMenu', function(e) {
switch (e.key) {
case 'Enter':
case ' ':
$this.hide();

e.preventDefault();
break;
e.preventDefault();
break;

case 'Escape':
$this.hide();
break;
};
});
case 'Escape':
$this.hide();
break;
};
});

this.bindCheckboxKeyEvents($this.togglerCheckboxInput);
this.configureSelectAllAria();
$this.togglerCheckboxInput.on('keydown.selectCheckboxMenu', function(e) {
if (e.key === 'Tab' && e.shiftKey) {
e.preventDefault();
}
}).on('keyup.selectCheckboxMenu', function(e) {
if (e.key === ' ') {
var input = $(this);
$this.toggleSelection(!input.prop('checked'));
e.preventDefault();
}
switch (e.key) {
case ' ':
var input = $(this);
$this.toggleSelection(!input.prop('checked'));
e.stopPropagation(); // GitHub #8340
e.preventDefault();
break;

case 'Tab':
if (e.shiftKey) {
e.preventDefault();
}
break;
};
}).on('change.selectCheckboxMenu', function(e) {
var input = $(this);
$this.toggleSelection(!input.prop('checked'));
Expand Down Expand Up @@ -1038,8 +1109,9 @@ PrimeFaces.widget.SelectCheckboxMenu = PrimeFaces.widget.BaseWidget.extend({

/**
* Brings up the overlay panel with the available checkbox options.
* @param {string} [focusSelector] The selector to use to focus the first item in the panel.
*/
show: function() {
show: function(focusSelector) {
var $this = this;

if (this.panel.is(":hidden") && this.transition) {
Expand All @@ -1053,6 +1125,8 @@ PrimeFaces.widget.SelectCheckboxMenu = PrimeFaces.widget.BaseWidget.extend({
$this.jq.attr('aria-expanded', true);
$this.postShow();
$this.bindPanelEvents();
focusSelector = focusSelector || '.ui-selectcheckboxmenu-item-input:focusable:first';
PrimeFaces.queueTask(() => $this.panel.find(focusSelector).focus());
}
});
}
Expand Down Expand Up @@ -1174,37 +1248,6 @@ PrimeFaces.widget.SelectCheckboxMenu = PrimeFaces.widget.BaseWidget.extend({
}
},

/**
* Sets up the keyboard event listeners for the given checkbox options.
* @private
* @param {JQuery} items Checkbo options for which to add the event listeners.
*/
bindCheckboxKeyEvents: function(items) {
var $this = this;

items.on('focus.selectCheckboxMenu', function(e) {
var input = $(this),
box = input.parent().next();

box.addClass('ui-state-focus');

PrimeFaces.scrollInView($this.itemContainerWrapper, box);
}).on('blur.selectCheckboxMenu', function(e) {
var input = $(this),
box = input.parent().next();

box.removeClass('ui-state-focus');
})
.on('keydown.selectCheckboxMenu', function(e) {
if (e.code === 'Space') {
e.preventDefault();
}
else if (e.key === 'Escape') {
$this.hide();
}
});
},

/**
* When multi mode is disabled: Upates the label that indicates the currently selected item.
* @private
Expand All @@ -1214,21 +1257,22 @@ PrimeFaces.widget.SelectCheckboxMenu = PrimeFaces.widget.BaseWidget.extend({
labelText = '';

if (checkedItems && checkedItems.length) {
for (var i = 0; i < checkedItems.length; i++) {
if (this.cfg.selectedLabel) {
labelText = this.cfg.selectedLabel;
break;
}
// Convert checkedItems to an array if it's not already
var checkedArray = Array.from(checkedItems);
// Get the values of the checked checkboxes and join them into a comma-separated string
var currentValuesString = checkedArray.map(item => item.value).join(',');
// screen reader set to current values to be read aloud
this.keyboardTarget.val(currentValuesString);

// Generate label text
labelText = this.cfg.selectedLabel
? this.cfg.selectedLabel
: checkedArray.map(item => $(item).next().text() || '').join(this.cfg.labelSeparator || '');

if (i > 0) {
labelText = labelText + this.cfg.labelSeparator;
}
labelText = labelText + ($(checkedItems[i]).next().text() || '');
}
this.labelContainer.addClass('ui-state-active');
}
else {
} else {
labelText = this.cfg.emptyLabel || this.defaultLabel || '';
this.keyboardTarget.val(labelText);
this.labelContainer.removeClass('ui-state-active');
}

Expand Down

0 comments on commit 5863954

Please sign in to comment.