6pac / SlickGrid

A lightning fast JavaScript grid/spreadsheet
https://github.com/6pac/SlickGrid/wiki
MIT License
1.84k stars 423 forks source link

ariaAttr support #206

Closed paraskt closed 6 years ago

paraskt commented 6 years ago

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,

ghiscoding commented 6 years ago

Seems related to issue #87

6pac commented 6 years ago

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 commented 6 years ago

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 6 years ago

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 commented 6 years ago

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

paraskt commented 6 years ago

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