New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ariaAttr support #206

Closed
paraskt opened this Issue Feb 27, 2018 · 6 comments

Comments

Projects
None yet
3 participants
@paraskt

paraskt commented Feb 27, 2018

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,

@paraskt paraskt changed the title from ariaAttr supoport to ariaAttr support Feb 27, 2018

@ghiscoding

This comment has been minimized.

Show comment
Hide comment
@ghiscoding

ghiscoding Feb 27, 2018

Collaborator

Seems related to issue #87

Collaborator

ghiscoding commented Feb 27, 2018

Seems related to issue #87

@6pac

This comment has been minimized.

Show comment
Hide comment
@6pac

6pac Feb 28, 2018

Owner

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.

Owner

6pac commented Feb 28, 2018

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.

@paraskt

This comment has been minimized.

Show comment
Hide comment
@paraskt

paraskt Feb 28, 2018

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).

paraskt commented Feb 28, 2018

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).

@paraskt

This comment has been minimized.

Show comment
Hide comment
@paraskt

paraskt Feb 28, 2018

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.

paraskt commented Feb 28, 2018

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.

@6pac

This comment has been minimized.

Show comment
Hide comment
@6pac

6pac Feb 28, 2018

Owner

Great, could you possibly post a sample of the editor?

Owner

6pac commented Feb 28, 2018

Great, could you possibly post a sample of the editor?

@paraskt

This comment has been minimized.

Show comment
Hide comment
@paraskt

paraskt Mar 1, 2018

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}
    }
}

paraskt commented Mar 1, 2018

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}
    }
}

@paraskt paraskt closed this Mar 1, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment