Skip to content

Added Optional Roving Tabindex to Toolbar #4563

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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Implement roving tabindex functionality in toolbar and picker components
  • Loading branch information
colfin-96 committed Jan 12, 2025
commit cfb866c84da2b4632b78b1c4f9283023237a7bbb
42 changes: 32 additions & 10 deletions packages/quill/src/modules/toolbar.ts
Original file line number Diff line number Diff line change
@@ -50,6 +50,11 @@ class Toolbar extends Module<ToolbarProps> {

// 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 = {};
@@ -140,28 +145,45 @@ class Toolbar extends Module<ToolbarProps> {
this.update(range);
});

if (this.hasRovingTabindex && input.tagName === 'BUTTON') {
input.addEventListener('keydown', (e) => {
this.handleKeyboardEvent(e);
});
}

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);
}
this.updateTabIndexes(target, e.key);
}
}

updateTabIndexes(target: EventTarget, key: string) {
const currentIndex = this.controls.findIndex(control => control[1] === target);
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];
currentItem.tabIndex = -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') {
34 changes: 0 additions & 34 deletions packages/quill/src/ui/picker.ts
Original file line number Diff line number Diff line change
@@ -96,9 +96,6 @@ class Picker {
// 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');
@@ -124,37 +121,6 @@ class Picker {
}
}

handleKeyboardEvent(e: KeyboardEvent) {
if (!this.hasRovingTabindex) return;
var target = e.currentTarget;
if (!target) return;

if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
this.updateTabIndexes(target, e.key);
}
}

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');
4 changes: 4 additions & 0 deletions packages/website/src/data/playground.tsx
Original file line number Diff line number Diff line change
@@ -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',
5 changes: 5 additions & 0 deletions packages/website/src/playground/roving-tabindex/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<div class="roving-tabindex">
<div id="editor"></div>
</div>

<script src="/index.js"></script>
11 changes: 11 additions & 0 deletions packages/website/src/playground/roving-tabindex/index.js
Original file line number Diff line number Diff line change
@@ -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'
});
12 changes: 12 additions & 0 deletions packages/website/src/playground/roving-tabindex/playground.json
Original file line number Diff line number Diff line change
@@ -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"
]
}