diff --git a/packages/quill/src/modules/toolbar.ts b/packages/quill/src/modules/toolbar.ts index a178d25936..d97c454ff2 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,15 @@ 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; + if (this.hasRovingTabindex) { + this.container.addEventListener('keydown', (e) => { + this.handleKeyboardEvent(e); + }); + } + this.controls = []; this.handlers = {}; if (this.options.handlers) { @@ -133,7 +144,66 @@ class Toolbar extends Module { } this.update(range); }); + this.controls.push([format, input]); + if (this.hasRovingTabindex) { + this.setTabIndexes(); + } + } + + setTabIndexes() { + this.controls.forEach((control, index) => { + const [, input] = control; + if (input.tagName === 'BUTTON') { + input.tabIndex = index === 0 ? 0 : -1; + } + }); + }; + + handleKeyboardEvent(e: KeyboardEvent) { + var target = e.currentTarget; + if (!target) return; + + if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') { + this.updateTabIndexes(target, e.key); + } + } + + updateTabIndexes(target: EventTarget, key: string) { + const elements = Array.from(this.container?.querySelectorAll('button, .ql-picker-label') || []) as HTMLElement[]; + + const currentIndex = elements.findIndex((el) => el.tabIndex === 0); + if (currentIndex === -1) return; + + const currentItem = this.controls[currentIndex][1]; + if (currentItem.tagName === 'SELECT') { + const qlPickerLabel = currentItem.previousElementSibling?.querySelectorAll('.ql-picker-label')[0]; + if (qlPickerLabel && qlPickerLabel.tagName === 'SPAN') { + (qlPickerLabel as HTMLElement).tabIndex = -1; + } + } else { + currentItem.tabIndex = -1; + } + + let nextIndex: number | null = null; + 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 === null) 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) { diff --git a/packages/quill/src/ui/picker.ts b/packages/quill/src/ui/picker.ts index d6b387c1a4..7b74df72bc 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(); }); @@ -85,14 +92,35 @@ class Picker { const label = document.createElement('span'); label.classList.add('ql-picker-label'); label.innerHTML = DropdownIcon; + + // Set tabIndex to -1 by default to prevent focus // @ts-expect-error - label.tabIndex = '0'; + label.tabIndex = '-1'; + + label.setAttribute('role', 'button'); label.setAttribute('aria-expanded', 'false'); this.container.appendChild(label); 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'); + }); + } + } + buildOptions() { const options = document.createElement('span'); options.classList.add('ql-picker-options'); @@ -180,7 +208,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 diff --git a/packages/website/src/data/playground.tsx b/packages/website/src/data/playground.tsx index 0d304dc56a..e684aebc4d 100644 --- a/packages/website/src/data/playground.tsx +++ b/packages/website/src/data/playground.tsx @@ -3,6 +3,10 @@ const playground = [ title: 'Basic setup with snow theme', url: '/playground/snow', }, + { + title: 'Basic setup with roving tabindex', + url: '/playground/roving-tabindex', + }, { title: 'Using Quill inside a form', url: '/playground/form', diff --git a/packages/website/src/playground/roving-tabindex/index.html b/packages/website/src/playground/roving-tabindex/index.html new file mode 100644 index 0000000000..ed57b133fc --- /dev/null +++ b/packages/website/src/playground/roving-tabindex/index.html @@ -0,0 +1,5 @@ +
+
+
+ + diff --git a/packages/website/src/playground/roving-tabindex/index.js b/packages/website/src/playground/roving-tabindex/index.js new file mode 100644 index 0000000000..95c9d4701c --- /dev/null +++ b/packages/website/src/playground/roving-tabindex/index.js @@ -0,0 +1,11 @@ +const quill = new Quill('#editor', { + modules: { + toolbar: [ + ['bold', 'italic', 'underline'], + [{ header: [1, 2, false] }], + ['image', 'code-block'], + ], + }, + placeholder: 'Compose an epic...', + theme: 'snow', // or 'bubble' +}); diff --git a/packages/website/src/playground/roving-tabindex/playground.json b/packages/website/src/playground/roving-tabindex/playground.json new file mode 100644 index 0000000000..b3b4681b77 --- /dev/null +++ b/packages/website/src/playground/roving-tabindex/playground.json @@ -0,0 +1,12 @@ +{ + "template": "static", + "externalResources": [ + "{{site.highlightjs}}/highlight.min.js", + "{{site.highlightjs}}/styles/atom-one-dark.min.css", + "{{site.katex}}/katex.min.js", + "{{site.katex}}/katex.min.css", + "{{site.cdn}}/quill.snow.css", + "{{site.cdn}}/quill.bubble.css", + "{{site.cdn}}/quill.js" + ] +}