From a8368415e602fd128b49172f8b4a9854852f5365 Mon Sep 17 00:00:00 2001 From: Colin Finger Date: Mon, 6 Jan 2025 15:41:13 +0100 Subject: [PATCH 1/4] ~ --- package-lock.json | 8 ++++---- packages/quill/.eslintrc.json | 3 ++- packages/quill/package.json | 2 +- packages/quill/src/modules/toolbar.ts | 1 + packages/quill/src/ui/picker.ts | 12 +++++++++++- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 23085e2961..49a90bc690 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "quill-monorepo", - "version": "2.0.3", + "version": "2.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "quill-monorepo", - "version": "2.0.3", + "version": "2.0.2", "license": "BSD-3-Clause", "workspaces": [ "packages/*" @@ -19236,7 +19236,7 @@ } }, "packages/quill": { - "version": "2.0.3", + "version": "2.0.3-beta.1", "license": "BSD-3-Clause", "dependencies": { "eventemitter3": "^5.0.1", @@ -19361,7 +19361,7 @@ } }, "packages/website": { - "version": "2.0.3", + "version": "2.0.2", "license": "BSD-3-Clause", "dependencies": { "@codesandbox/sandpack-react": "^2.11.3", diff --git a/packages/quill/.eslintrc.json b/packages/quill/.eslintrc.json index edf5dc5f30..8d0f04b2ce 100644 --- a/packages/quill/.eslintrc.json +++ b/packages/quill/.eslintrc.json @@ -36,7 +36,8 @@ "@typescript-eslint/ban-types": "off", "@typescript-eslint/no-explicit-any": "off", "import/no-named-as-default-member": "off", - "prefer-arrow-callback": "error" + "prefer-arrow-callback": "error", + "endOfLine": "auto" } } ] diff --git a/packages/quill/package.json b/packages/quill/package.json index 4dd6a2e662..858ee53a8b 100644 --- a/packages/quill/package.json +++ b/packages/quill/package.json @@ -1,6 +1,6 @@ { "name": "quill", - "version": "2.0.3", + "version": "2.0.3-beta.2", "description": "Your powerful, rich text editor", "author": "Jason Chen ", "homepage": "https://quilljs.com", diff --git a/packages/quill/src/modules/toolbar.ts b/packages/quill/src/modules/toolbar.ts index a178d25936..9c515e2495 100644 --- a/packages/quill/src/modules/toolbar.ts +++ b/packages/quill/src/modules/toolbar.ts @@ -133,6 +133,7 @@ class Toolbar extends Module { } this.update(range); }); + input.tabIndex = 123; this.controls.push([format, input]); } diff --git a/packages/quill/src/ui/picker.ts b/packages/quill/src/ui/picker.ts index d6b387c1a4..011043fb57 100644 --- a/packages/quill/src/ui/picker.ts +++ b/packages/quill/src/ui/picker.ts @@ -34,6 +34,9 @@ class Picker { this.escape(); event.preventDefault(); break; + case 'ArrowLeft': + case 'ArrowRight': + this.toggleTabIndex(); default: } }); @@ -48,6 +51,10 @@ class Picker { toggleAriaAttribute(this.options, 'aria-hidden'); } + toggleTabIndex() { + this.label.tabIndex = this.label.tabIndex === 0 ? -1 : 0; + } + buildItem(option: HTMLOptionElement) { const item = document.createElement('span'); // @ts-expect-error @@ -85,8 +92,11 @@ class Picker { const label = document.createElement('span'); label.classList.add('ql-picker-label'); label.innerHTML = DropdownIcon; + + // TODO: @cofi set all tabindex to -1 initially and then per JS set first one to 0. Then per keyboard right/left navigation set the next/prev to 0 and the rest to -1 // @ts-expect-error - label.tabIndex = '0'; + label.tabIndex = '-1'; + label.setAttribute('role', 'button'); label.setAttribute('aria-expanded', 'false'); this.container.appendChild(label); From 94ea34e6e66d3ea94a97bc1ad4ea34a000a62e6c Mon Sep 17 00:00:00 2001 From: colfin-96 Date: Tue, 7 Jan 2025 22:32:06 +0100 Subject: [PATCH 2/4] Implement roving tabindex for toolbar and picker components --- packages/quill/src/modules/toolbar.ts | 52 ++++++++++++++++++- packages/quill/src/ui/picker.ts | 73 +++++++++++++++++++++++---- 2 files changed, 115 insertions(+), 10 deletions(-) diff --git a/packages/quill/src/modules/toolbar.ts b/packages/quill/src/modules/toolbar.ts index 9c515e2495..b522925c6b 100644 --- a/packages/quill/src/modules/toolbar.ts +++ b/packages/quill/src/modules/toolbar.ts @@ -27,6 +27,8 @@ class Toolbar extends Module { controls: [string, HTMLElement][]; handlers: Record; + hasRovingTabindex: boolean = false; + constructor(quill: Quill, options: Partial) { super(quill, options); if (Array.isArray(this.options.container)) { @@ -45,6 +47,10 @@ class Toolbar extends Module { return; } this.container.classList.add('ql-toolbar'); + + // Check if the parent element has the custom "roving-tabindex" class in order to enable or disable roving tabindex + this.hasRovingTabindex = this.container.closest('.roving-tabindex') !== null; + this.controls = []; this.handlers = {}; if (this.options.handlers) { @@ -133,10 +139,54 @@ class Toolbar extends Module { } this.update(range); }); - input.tabIndex = 123; + + if (this.hasRovingTabindex && input.tagName === 'BUTTON') { + input.addEventListener('keydown', (e) => { + this.handleKeyboardEvent(e); + }); + } + this.controls.push([format, input]); } + handleKeyboardEvent(e: KeyboardEvent) { + var target = e.currentTarget; + if (!target) return; + + switch (e.key) { + case 'ArrowLeft': + case 'ArrowRight': + this.updateTabIndexes(target, e.key); + break; + } + } + + updateTabIndexes(target: EventTarget, key: string) { + const currentIndex = this.controls.findIndex(control => control[1] === target); + const currentItem = this.controls[currentIndex][1]; + currentItem.tabIndex = -1; + + let nextIndex; + if (key === 'ArrowLeft') { + nextIndex = currentIndex === 0 ? this.controls.length - 1 : currentIndex - 1; + } else if (key === 'ArrowRight') { + nextIndex = currentIndex === this.controls.length - 1 ? 0 : currentIndex + 1; + } + + if (nextIndex === undefined) return; + const nextItem = this.controls[nextIndex][1]; + if (nextItem.tagName === 'SELECT') { + const qlPickerLabel = nextItem.previousElementSibling?.querySelectorAll('.ql-picker-label')[0]; + if (qlPickerLabel && qlPickerLabel.tagName === 'SPAN') { + (qlPickerLabel as HTMLElement).tabIndex = 0; + (qlPickerLabel as HTMLElement).focus(); + } + } else { + nextItem.tabIndex = 0; + nextItem.focus(); + } + } + update(range: Range | null) { const formats = range == null ? {} : this.quill.getFormat(range); this.controls.forEach((pair) => { diff --git a/packages/quill/src/ui/picker.ts b/packages/quill/src/ui/picker.ts index 011043fb57..4f6d6a2c48 100644 --- a/packages/quill/src/ui/picker.ts +++ b/packages/quill/src/ui/picker.ts @@ -14,6 +14,8 @@ class Picker { container: HTMLElement; label: HTMLElement; + hasRovingTabindex: boolean = false; + constructor(select: HTMLSelectElement) { this.select = select; this.container = document.createElement('span'); @@ -22,6 +24,11 @@ class Picker { // @ts-expect-error Fix me later this.select.parentNode.insertBefore(this.container, this.select); + // Set tabIndex for the first item in the toolbar + this.hasRovingTabindex = this.container.closest('.roving-tabindex') !== null; + this.setTabIndexes(); + + this.label.addEventListener('mousedown', () => { this.togglePicker(); }); @@ -34,9 +41,6 @@ class Picker { this.escape(); event.preventDefault(); break; - case 'ArrowLeft': - case 'ArrowRight': - this.toggleTabIndex(); default: } }); @@ -51,10 +55,6 @@ class Picker { toggleAriaAttribute(this.options, 'aria-hidden'); } - toggleTabIndex() { - this.label.tabIndex = this.label.tabIndex === 0 ? -1 : 0; - } - buildItem(option: HTMLOptionElement) { const item = document.createElement('span'); // @ts-expect-error @@ -93,9 +93,13 @@ class Picker { label.classList.add('ql-picker-label'); label.innerHTML = DropdownIcon; - // TODO: @cofi set all tabindex to -1 initially and then per JS set first one to 0. Then per keyboard right/left navigation set the next/prev to 0 and the rest to -1 + // Set tabIndex to -1 by default to prevent focus // @ts-expect-error label.tabIndex = '-1'; + label.addEventListener('keydown', (event) => { + this.handleKeyboardEvent(event); + }); + label.setAttribute('role', 'button'); label.setAttribute('aria-expanded', 'false'); @@ -103,6 +107,57 @@ class Picker { return label; } + setTabIndexes() { + const toolbar = this.select.closest('.ql-toolbar'); + if (!toolbar) return; + const items = Array.from(toolbar.querySelectorAll('.ql-picker .ql-picker-label, .ql-toolbar button')); + + if (this.hasRovingTabindex) { + if (items[0] === this.label) { + items[0].setAttribute('tabindex', '0') + } + + } else { + items.forEach((item) => { + item.setAttribute('tabindex', '0'); + }); + } + } + + handleKeyboardEvent(e: KeyboardEvent) { + if (!this.hasRovingTabindex) return; + var target = e.currentTarget; + if (!target) return; + + switch (e.key) { + case 'ArrowLeft': + case 'ArrowRight': + this.updateTabIndexes(target, e.key); + break; + } + } + + updateTabIndexes(target: EventTarget, key: string) { + this.label.setAttribute('tabindex', '-1'); + + const toolbar = this.container.closest('.ql-toolbar'); + if (!toolbar) return; + const items = Array.from(toolbar.querySelectorAll('.ql-picker .ql-picker-label, .ql-toolbar button')); + const currentIndex = items.indexOf(target as HTMLElement); + let newIndex; + + if (key === 'ArrowLeft') { + newIndex = (currentIndex - 1 + items.length) % items.length; + } else if (key === 'ArrowRight') { + newIndex = (currentIndex + 1) % items.length; + } + + if (!newIndex) return; + + items[newIndex].setAttribute('tabindex', '0'); + (items[newIndex] as HTMLElement).focus(); + } + buildOptions() { const options = document.createElement('span'); options.classList.add('ql-picker-options'); @@ -190,7 +245,7 @@ class Picker { const item = // @ts-expect-error Fix me later this.container.querySelector('.ql-picker-options').children[ - this.select.selectedIndex + this.select.selectedIndex ]; option = this.select.options[this.select.selectedIndex]; // @ts-expect-error From d3770d55e08313835a974eb4952c302a3da80359 Mon Sep 17 00:00:00 2001 From: colfin-96 Date: Tue, 7 Jan 2025 22:35:11 +0100 Subject: [PATCH 3/4] Revert version to 2.0.3 and remove endOfLine rule from ESLint configuration --- packages/quill/.eslintrc.json | 3 +-- packages/quill/package.json | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/quill/.eslintrc.json b/packages/quill/.eslintrc.json index 8d0f04b2ce..edf5dc5f30 100644 --- a/packages/quill/.eslintrc.json +++ b/packages/quill/.eslintrc.json @@ -36,8 +36,7 @@ "@typescript-eslint/ban-types": "off", "@typescript-eslint/no-explicit-any": "off", "import/no-named-as-default-member": "off", - "prefer-arrow-callback": "error", - "endOfLine": "auto" + "prefer-arrow-callback": "error" } } ] diff --git a/packages/quill/package.json b/packages/quill/package.json index 858ee53a8b..4dd6a2e662 100644 --- a/packages/quill/package.json +++ b/packages/quill/package.json @@ -1,6 +1,6 @@ { "name": "quill", - "version": "2.0.3-beta.2", + "version": "2.0.3", "description": "Your powerful, rich text editor", "author": "Jason Chen ", "homepage": "https://quilljs.com", From 06661f7b87102cbbcd3bf35ab87482b7c1aae917 Mon Sep 17 00:00:00 2001 From: colfin-96 Date: Tue, 7 Jan 2025 22:35:51 +0100 Subject: [PATCH 4/4] Bump version to 2.0.3 in package-lock.json --- package-lock.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 49a90bc690..23085e2961 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "quill-monorepo", - "version": "2.0.2", + "version": "2.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "quill-monorepo", - "version": "2.0.2", + "version": "2.0.3", "license": "BSD-3-Clause", "workspaces": [ "packages/*" @@ -19236,7 +19236,7 @@ } }, "packages/quill": { - "version": "2.0.3-beta.1", + "version": "2.0.3", "license": "BSD-3-Clause", "dependencies": { "eventemitter3": "^5.0.1", @@ -19361,7 +19361,7 @@ } }, "packages/website": { - "version": "2.0.2", + "version": "2.0.3", "license": "BSD-3-Clause", "dependencies": { "@codesandbox/sandpack-react": "^2.11.3",