From a66317c15bb4ea907ab54a61d8b276cca845c315 Mon Sep 17 00:00:00 2001 From: CodoPixel Date: Fri, 23 Apr 2021 22:58:08 +0200 Subject: [PATCH] Better digital accessibility --- src/JSPanel.js | 95 ++++++++++++++++++++++++++++++++++---------- src/JSPanel.ts | 96 +++++++++++++++++++++++++++++++++++---------- src/panel-style.css | 24 ++++++------ 3 files changed, 162 insertions(+), 53 deletions(-) diff --git a/src/JSPanel.js b/src/JSPanel.js index c85679f..e6cae71 100644 --- a/src/JSPanel.js +++ b/src/JSPanel.js @@ -25,6 +25,7 @@ class JSPanel { } /** * Builds the panel. + * @private */ _buildPanel() { const top = this.options.top === undefined ? null : this.options.top + "px"; @@ -54,12 +55,12 @@ class JSPanel { // items // if (this.options.items) { - const ul = document.createElement("ul"); + const container = this._createEl("div", { className: "container-items" }); for (let item of this.options.items) { const built_item = this._buildItem(item); - ul.appendChild(built_item); + container.appendChild(built_item); } - this.panel.appendChild(ul); + this.panel.appendChild(container); } else { throw new Error("You need to define items to be displayed in the panel."); @@ -70,7 +71,7 @@ class JSPanel { document.addEventListener("click", (e) => { const target = e.target; if (target && this.panel) { - if (!this.panel.contains(target) && this._isOpened()) { + if (!this.panel.contains(target) && this._isOpen()) { this._closePanel(); } } @@ -97,7 +98,7 @@ class JSPanel { * @returns {boolean} True if the panel is opened. * @private */ - _isOpened() { + _isOpen() { if (this.panel) { return !this.panel.classList.contains("panel-hidden"); } @@ -113,19 +114,44 @@ class JSPanel { _togglePanel(e) { if (this.button && this.panel) { e.stopPropagation(); - if (this._isOpened()) { + if (this._isOpen()) { this._closePanel(); } else { this.button.setAttribute("aria-expanded", "true"); this.panel.classList.remove("panel-hidden"); - // Digital accessibility - const all_items = this.panel.querySelectorAll("li"); + const all_items = this._getAllItems(); if (all_items && all_items[0]) all_items[0].focus(); } } } + /** + * Gets all the items from the panel if it's open. + * @returns {NodeListOf|null} All the items. + */ + _getAllItems() { + if (this._isOpen()) { + return this.panel.querySelectorAll("button"); + } + else { + return null; + } + } + /** + * Gets all the active items from the panel if it's open. + * @returns {Array|null} All the items that have an onclick property. + */ + _getAllActiveItems() { + if (this._isOpen()) { + const active_elements = Array.from(this.panel.querySelectorAll("button")); + active_elements.push(this.button); + return active_elements; + } + else { + return null; + } + } /** * Closes the panel. * @private @@ -185,41 +211,70 @@ class JSPanel { return div; } else { - const li = this._createEl("li"); - li.setAttribute("tabindex", "0"); + const button = this._createEl("button"); + button.setAttribute("aria-label", item.title); + button.setAttribute("tabindex", "0"); if ((item.icon && !item.fontawesome_icon) || (item.icon && item.fontawesome_icon)) { const icon = this._createEl("img", { attributes: [["src", item.icon]] }); - li.appendChild(icon); + button.appendChild(icon); } else if (!item.icon && item.fontawesome_icon) { const icon = this._createEl("i", { className: item.fontawesome_icon }); if (item.fontawesome_color) icon.style.color = item.fontawesome_color; - li.appendChild(icon); + button.appendChild(icon); } if (item.className) { const classes = item.className.split(" "); for (let clas of classes) { - li.classList.add(clas); + button.classList.add(clas); } } if (item.attributes) { for (let attr of item.attributes) { const name = attr[0]; const value = attr[1]; - li.setAttribute(name, value); + button.setAttribute(name, value); } } - if (item.title) { - const title = this._createEl("span", { textContent: item.title }); - li.appendChild(title); - } - li.addEventListener('click', () => { + const title = this._createEl("span", { textContent: item.title }); + button.appendChild(title); + button.addEventListener('click', () => { if (item.onclick) item.onclick(); this._closePanel(); }); - return li; + button.addEventListener("keydown", (e) => { + if (e.key === "Tab" || e.keyCode === 9) { + if (this._isOpen()) + this._focusInPanel(e); + } + }); + return button; + } + } + /** + * Blocks the focus inside the panel while it's open. + * @param {KeyboardEvent} e The keyboard event. + */ + _focusInPanel(e) { + const all_items = this._getAllActiveItems(); + if (all_items) { + e.preventDefault(); + let index = Array.from(all_items).findIndex(f => this.panel ? f === this.panel.querySelector(":focus") : false); + if (e.shiftKey === true) { + index--; + } + else { + index++; + } + if (index >= all_items.length) { + index = 0; + } + if (index < 0) { + index = all_items.length - 1; + } + all_items[index].focus(); } } /** diff --git a/src/JSPanel.ts b/src/JSPanel.ts index 315951c..40878f4 100644 --- a/src/JSPanel.ts +++ b/src/JSPanel.ts @@ -74,6 +74,7 @@ class JSPanel { /** * Builds the panel. + * @private */ private _buildPanel(): void { const top = this.options.top === undefined ? null : this.options.top + "px"; @@ -101,12 +102,12 @@ class JSPanel { // if (this.options.items) { - const ul = document.createElement("ul"); + const container = this._createEl("div", { className: "container-items" }); for (let item of this.options.items) { const built_item = this._buildItem(item); - ul.appendChild(built_item); + container.appendChild(built_item); } - this.panel.appendChild(ul); + this.panel.appendChild(container); } else { throw new Error("You need to define items to be displayed in the panel."); } @@ -118,7 +119,7 @@ class JSPanel { document.addEventListener("click", (e) => { const target = e.target as HTMLElement; if (target && this.panel) { - if (!this.panel.contains(target) && this._isOpened()) { + if (!this.panel.contains(target) && this._isOpen()) { this._closePanel(); } } @@ -148,7 +149,7 @@ class JSPanel { * @returns {boolean} True if the panel is opened. * @private */ - private _isOpened(): boolean { + private _isOpen(): boolean { if (this.panel) { return !this.panel.classList.contains("panel-hidden"); } else { @@ -165,20 +166,44 @@ class JSPanel { if (this.button && this.panel) { e.stopPropagation(); - if (this._isOpened()) { + if (this._isOpen()) { this._closePanel(); } else { this.button.setAttribute("aria-expanded", "true"); this.panel.classList.remove("panel-hidden"); - // Digital accessibility - - const all_items = this.panel.querySelectorAll("li"); + const all_items = this._getAllItems(); if (all_items && all_items[0]) all_items[0].focus(); } } } + /** + * Gets all the items from the panel if it's open. + * @returns {NodeListOf|null} All the items. + */ + private _getAllItems(): NodeListOf | null { + if (this._isOpen()) { + return (this.panel as HTMLElement).querySelectorAll("button"); + } else { + return null; + } + } + + /** + * Gets all the active items from the panel if it's open. + * @returns {Array|null} All the items that have an onclick property. + */ + private _getAllActiveItems(): Element[] | null { + if (this._isOpen()) { + const active_elements: HTMLElement[] = Array.from((this.panel as HTMLElement).querySelectorAll("button")); + active_elements.push(this.button as HTMLElement); + return active_elements; + } else { + return null; + } + } + /** * Closes the panel. * @private @@ -240,22 +265,23 @@ class JSPanel { const div = this._createEl("div", { className: 'jspanel-separator' }); return div; } else { - const li = this._createEl("li"); - li.setAttribute("tabindex", "0"); + const button = this._createEl("button"); + button.setAttribute("aria-label", item.title); + button.setAttribute("tabindex", "0"); if ((item.icon && !item.fontawesome_icon) || (item.icon && item.fontawesome_icon)) { const icon = this._createEl("img", { attributes: [["src", item.icon]] }); - li.appendChild(icon); + button.appendChild(icon); } else if (!item.icon && item.fontawesome_icon) { const icon = this._createEl("i", { className: item.fontawesome_icon }); if (item.fontawesome_color) icon.style.color = item.fontawesome_color; - li.appendChild(icon); + button.appendChild(icon); } if (item.className) { const classes = item.className.split(" "); for (let clas of classes) { - li.classList.add(clas); + button.classList.add(clas); } } @@ -263,21 +289,49 @@ class JSPanel { for (let attr of item.attributes) { const name = attr[0]; const value = attr[1]; - li.setAttribute(name, value); + button.setAttribute(name, value); } } - if (item.title) { - const title = this._createEl("span", { textContent: item.title }); - li.appendChild(title); - } + const title = this._createEl("span", { textContent: item.title }); + button.appendChild(title); - li.addEventListener('click', () => { + button.addEventListener('click', () => { if (item.onclick) item.onclick(); this._closePanel(); }); - return li; + button.addEventListener("keydown", (e: KeyboardEvent) => { + if (e.key === "Tab" || e.keyCode === 9) { + if (this._isOpen()) this._focusInPanel(e); + } + }); + + return button; + } + } + + /** + * Blocks the focus inside the panel while it's open. + * @param {KeyboardEvent} e The keyboard event. + */ + private _focusInPanel(e: KeyboardEvent): void { + const all_items = this._getAllActiveItems(); + if (all_items) { + e.preventDefault(); + let index = Array.from(all_items).findIndex(f => this.panel ? f === this.panel.querySelector(":focus") : false); + if (e.shiftKey === true) { + index--; + } else { + index++; + } + if (index >= all_items.length) { + index = 0; + } + if(index < 0) { + index = all_items.length - 1; + } + (all_items[index] as HTMLElement).focus(); } } diff --git a/src/panel-style.css b/src/panel-style.css index 3fcaa41..35d17f3 100644 --- a/src/panel-style.css +++ b/src/panel-style.css @@ -29,10 +29,8 @@ } /* the list of items */ -.jspanel ul { - list-style-type: none; - margin: 0; - padding: 0; +.jspanel .container-items { + box-sizing: border-box; display:flex; flex-direction: column; justify-content: flex-start; @@ -40,7 +38,9 @@ } /* for each item */ -.jspanel li { +.jspanel button { + border: none; + cursor: pointer; width: 100%; height: 30px; box-sizing: border-box; @@ -51,23 +51,23 @@ align-items: center; } -.jspanel li:hover { +.jspanel button:hover { background-color: var(--panel-hover-item-background-color, #f4f6fa); color: var(--panel-hover-item-color, #385074); } -.jspanel li:focus { +.jspanel button:focus { outline: auto; } /* the title of the item */ -.jspanel li span { +.jspanel button span { pointer-events: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; - display: block; + display: flex; width: 100%; text-overflow: ellipsis; white-space: nowrap; @@ -75,8 +75,8 @@ } /* img is an `icon`, i is a `fontawesome_icon` */ -.jspanel li img, -.jspanel li i { +.jspanel button img, +.jspanel button i { pointer-events: none; -webkit-user-select: none; -moz-user-select: none; @@ -86,7 +86,7 @@ } /* the icon */ -.jspanel li img { +.jspanel button img { width: var(--panel-icon-width, 13px); height: auto; }