Closed paraskt closed 6 years ago
Seems related to issue #87
Indeed.
So @paraskt, have you looked at the ARIA enhancements in that branch? Do you know enough about accessibility to tell me if this is still a relevant and modern methodology?
These things move on so quickly, I'm reluctant to just apply that code without background research. I've had a look at some ARIA educational sites, but frankly it appears to be quite a complex topic to get up to speed on, though the required changes to the HTML are probably simple if you know what you're doing.
Yes, ARIA attributes are the most reliable way to tell the screen reader about an HTML element. All the screen readers rely on ARIA attributes and "role" to determine the element type and how to process them. Currently, If I tab through Slickgrid cells, screen readers don't read anything. With the changes in the another branch things appear to work fine (even narrator read properly).
I worked around this by creating custom editor for cells and having aria properties over there. On editor init I put the focus on the editor element (which is identical to the cell content), this makes screen reader read the aria-properties. Not sure if this is the right approach, but it is working.
Great, could you possibly post a sample of the editor?
For my scenario, I was just interested in aria-label (Screen reader will read this) and role attributes. I marked all the columns as editable and set autoedit to true. Then, I created a custom editor for the cells (which mimics the UX of the cell) and programmatically put the focus on the first tabbable element. I also removed the tabbed navigation from the grid, tabbing is now used for navigation inside the cell.
Moving on to cell brings up the editor with element in focus, which screen reader recognizes and then reads it properly. The only issue that I had was, if editor is visible, onClick of slick grid was not working. So, handled this scenario separately from the editor itself.
Here is the trimmed down version of my code.
Slickgrid column definition:
public columns = [
// tslint:disable-next-line:typedef no-any only-arrow-functions object-literal-shorthand
{
id: 'message', name: stringResources.Monitoring_ErrorLabel, field: 'message',
width: this.columnWidths[7], minWidth: 45, editable: true, editor: CustomEditor.bind(this, this.eventHandlers),
formatter: (_row: number, _column: number, value: string) => {
const messageIcon = (value) ? '<div id="runMessage" class="svg-i-16x16-comment"></div>' : '';
return '<div class="gridIcons">' + messageIcon + '</div>';
}
},
{
id: 'runId', name: stringResources.Monitoring_RunIDLabel, field: 'runId', width: this.columnWidths[8] + this.columnPadding,
minWidth: 45, editable: true, editor: CustomEditor.bind(this, this.eventHandlers),
formatter: (_row: number, _column: number, value: string) => {
return CustomEditor.getFormattedRunId(value);
}
}
]
My custom editor:
export class CustomEditor {
// tslint:disable-next-line:no-any
public args: any;
public focussableElements: NodeListOf<Element>;
public focussedElementIndex: number = -1;
public eventHandlers: { [eventKey: string]: (item: object, event: MouseEvent|KeyboardEvent) => void };
public static getFormattedMessage(message: string): string {
const messageIcon = (message) ? '<div id="runMessage" class="svg-i-16x16-comment" role="button" tabindex="0" title="'+ stringResources.Error +'"></div>' : '';
return '<div class="gridIcons">' + messageIcon + '</div>';
}
public static getFormattedRunId(id: string): string {
return '<div aria-label="' + stringResources.Monitoring_RunIDLabel + ' ' + id +'" tabindex="0">' + id + '</div>';
}
// tslint:disable-next-line:no-any
public constructor(handlers: any, args: any) {
// initialize the cell editor UI
if (!args || !args.container || !args.item || !args.column) {
return;
}
let divElement: HTMLElement;
this.args = args;
this.eventHandlers = handlers;
switch (args.column.id) {
case 'message':
divElement = this.createCellEditorContainer()
// tslint:disable-next-line:no-inner-html
divElement.innerHTML = CustomEditor.getFormattedMessage(args.item.message);
break;
case 'runId':
divElement = this.createCellEditorContainer('gridCellEditorContent')
// tslint:disable-next-line:no-inner-html
divElement.innerHTML = CustomEditor.getFormattedRunId(args.item.runId);
break;
default:
return;
}
this.args.container.appendChild(divElement);
this.focussableElements = this.args.container.querySelectorAll('[tabindex="0"]');
this.setNextFocus();
}
public createCellEditorContainer(cssClass?: string): HTMLElement {
let divElement = document.createElement('DIV');
if (cssClass) {
divElement.className = cssClass;
}
divElement.onkeydown = this.handleKeyDown.bind(this);
divElement.onclick = this.handleClick.bind(this);
return divElement;
}
public setNextFocus(): void {
if (this.focussableElements && this.focussableElements.length) {
this.focussedElementIndex = (this.focussedElementIndex + this.focussableElements.length + 1) % this.focussableElements.length;
const nextElement = this.focussableElements[this.focussedElementIndex] as HTMLElement;
if (nextElement) {
nextElement.focus();
}
}
}
public handleKeyDown(event: KeyboardEvent): void {
if (KeyboardUtils.IsKeyboardTabEvent(event)) {
this.setNextFocus();
event.preventDefault();
event.stopPropagation();
}
}
public handleClick(e: MouseEvent): void {
const handler = this.eventHandlers[(event.target as HTMLElement).id];
if (handler && this.args && this.args.item) {
handler(this.args.item as {}, event as MouseEvent);
}
}
/*********** REQUIRED METHODS ***********/
public destroy(): void {
// remove all data, events & dom elements created in the constructor
while (this.args && this.args.container && this.args.container.firstChild) {
this.args.container.removeChild(this.args.container.firstChild);
}
}
public focus(): void {
// set the focus on the main input control (if any)
}
public isValueChanged(): boolean {
// return true if the value(s) being edited by the user has/have been changed
return false;
}
public serializeValue(): string {
// return the value(s) being edited by the user in a serialized form
// can be an arbitrary object
// the only restriction is that it must be a simple object that can be passed around even
// when the editor itself has been destroyed
return '';
}
// tslint:disable-next-line:no-any
public loadValue(item: any): void {
// load the value(s) from the data item and update the UI
// this method will be called immediately after the editor is initialized
// it may also be called by the grid if if the row/cell being edited is updated via grid.updateRow/updateCell
}
// tslint:disable-next-line:no-any
public applyValue(item: any, state: any): void {
// deserialize the value(s) saved to "state" and apply them to the data item
// this method may get called after the editor itself has been destroyed
// treat it as an equivalent of a Java/C# "static" method - no instance variables should be accessed
}
// tslint:disable-next-line:no-any
public validate(): any {
// validate user input and return the result along with the validation message, if any
// if the input is valid, return {valid:true,msg:null}
}
}
Hi, I am trying to make slickgrid cells, screen-reader accessible. ariaAttr support was added for this in another branch. I dont think that change has merged over here, Is there a plan to get those changes?
Thanks,