-
Notifications
You must be signed in to change notification settings - Fork 1
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
Implement Tabs block plugin #12
base: trunk
Are you sure you want to change the base?
Conversation
I was able to implement basic interactions for tabs in the editor: July.16th.progress.report.movI'm having some issues with generating the initial tabs as it takes the input and creates the number requested, but I need to wrap up for the day. The rest is working pretty much like I intended. |
Scaffolded the initial structure for the plugin.
Thanks for the update! It's a great start. Just to be clear, will the block display tabs side-by-side in the final UX? That's pretty important to get right because the current layout doesn't resemble the tabbed layout most users would expect. I think we can skip the placeholder state asking the user about the number of tabs and default to two. It's pretty easy to duplicate a tab or or add a new one and getting rid of that extra step will help users see the block faster. Sidenote: there's a linked issue in the GH repo (WordPress/gutenberg#34079). I wonder if we should flag that the block is being worked on in this repo so it doesn't get picked up there unnecessarily. |
e2271ca - seems to work as expected 😄 Progress.July.17th.mov |
I think this is relevant for "my" Marquee block, too. I noticed that when I don't add any inner blocks, it appears like an empty space after a reload. I'll add a placeholder too. |
That sounds like a crutch. Can we improve that behavior in core? The Tabs block will have inner blocks so I wonder if we need the placeholder given we can insert the block with two |
I'm sure we could improve it directly in WP Core. For this prototype, it's fine to leave it as is with the block variation for now, as it completely resolves the issue, and the inserted block has two tabs, as expected. I made further progress with tabs in the editor. I have more interactions in place. I was looking at accessibility requirements for Tabs as part of the exercise, and there are multiple considerations to take into account when working on the final product. I set some essential HTML attributes and copied styles from https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-automatic/ as a temporary measure. This is how it looks in the editor: Progress.July.18th.movIn my opinion, the biggest challenge in the editor now is to ensure that keyboard navigation works correctly and it is possible to use arrow keys (left and right) and home/end to switch between the active tabs. Currently, it works as expected, only with a mouse or trackpad. I'm still unclear on what to do about that list of tabs and whether to try to save them to the database, or whether to print them on the server during rendering. Moving forward, the challenge I see is that individual tabs can be configured separately by providing the title and the icon for the tab, but the question is whether it's a good idea to lift all those settings up to the Tabs block, or it's possible to store them in individual Tab blocks, but somehow lift them up when serializing the Tabs block to the database. |
Good progress! Love to see to the block take shape.
As far as accessibility, there are a few reasonable tips in this article. I think the general approach is to let users navigate using the Tab key and active a tab by pressing Enter. |
I'm not worried about the frontend part as it's relatively simple to reimplement with Interactivity API based on plain JS version shared in the Codepen link included at https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-automatic/: class TabsManual {
constructor(groupNode) {
this.tablistNode = groupNode;
this.tabs = [];
this.firstTab = null;
this.lastTab = null;
this.tabs = Array.from(this.tablistNode.querySelectorAll('[role=tab]'));
this.tabpanels = [];
for (var i = 0; i < this.tabs.length; i += 1) {
var tab = this.tabs[i];
var tabpanel = document.getElementById(tab.getAttribute('aria-controls'));
tab.tabIndex = -1;
tab.setAttribute('aria-selected', 'false');
this.tabpanels.push(tabpanel);
tab.addEventListener('keydown', this.onKeydown.bind(this));
tab.addEventListener('click', this.onClick.bind(this));
if (!this.firstTab) {
this.firstTab = tab;
}
this.lastTab = tab;
}
this.setSelectedTab(this.firstTab);
}
setSelectedTab(currentTab) {
for (var i = 0; i < this.tabs.length; i += 1) {
var tab = this.tabs[i];
if (currentTab === tab) {
tab.setAttribute('aria-selected', 'true');
tab.removeAttribute('tabindex');
this.tabpanels[i].classList.remove('is-hidden');
} else {
tab.setAttribute('aria-selected', 'false');
tab.tabIndex = -1;
this.tabpanels[i].classList.add('is-hidden');
}
}
}
moveFocusToTab(currentTab) {
currentTab.focus();
}
moveFocusToPreviousTab(currentTab) {
var index;
if (currentTab === this.firstTab) {
this.moveFocusToTab(this.lastTab);
} else {
index = this.tabs.indexOf(currentTab);
this.moveFocusToTab(this.tabs[index - 1]);
}
}
moveFocusToNextTab(currentTab) {
var index;
if (currentTab === this.lastTab) {
this.moveFocusToTab(this.firstTab);
} else {
index = this.tabs.indexOf(currentTab);
this.moveFocusToTab(this.tabs[index + 1]);
}
}
/* EVENT HANDLERS */
onKeydown(event) {
var tgt = event.currentTarget,
flag = false;
switch (event.key) {
case 'ArrowLeft':
this.moveFocusToPreviousTab(tgt);
flag = true;
break;
case 'ArrowRight':
this.moveFocusToNextTab(tgt);
flag = true;
break;
case 'Home':
this.moveFocusToTab(this.firstTab);
flag = true;
break;
case 'End':
this.moveFocusToTab(this.lastTab);
flag = true;
break;
default:
break;
}
if (flag) {
event.stopPropagation();
event.preventDefault();
}
}
// Since this example uses buttons for the tabs, the click onr also is activated
// with the space and enter keys
onClick(event) {
this.setSelectedTab(event.currentTarget);
}
}
// Initialize tablist
window.addEventListener('load', function () {
var tablists = document.querySelectorAll('[role=tablist].manual');
for (var i = 0; i < tablists.length; i++) {
new TabsManual(tablists[i]);
}
}); I was worried about interactions in the editor that will allow to use keyboard to switch between tabs in the editor's canvas and edit the title of the tab. I suspect that is going to be the more time consuming. |
I see what you mean. Could it work exactly like the Button or Group block with nested content? You'd use arrow keys to navigate between tab content.
|
…dex is properly serialized
…nd JS interactions
I did another shortcut and copied the adjusted JavaScript implementation from ARIA Authoring Practices Guide shared in #12 (comment). This is how the entire experience looks like with keyboard interactions tracked in the screencast: Progress.July.19th.movAs for the frontend, I fully replicated what was included at https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-manual/, so that might be considered close to ready. However, we would need to have a closer look at the following aspects next:
In the editor, the user interactions are way more complex to do right. It's even visible in the screencast recorded, as I didn't record the happy path 😄 Some of the questions and further action items I noted:
|
I'd say yes. It makes the editing experience kind of self-explanatory.
It'd start with defaulting to the first one and reiterate based on user feedback. Thanks for doing this work, @gziolo. It's amazing how much you've built in such a short time. 🙇 |
Looks like this was going to head over to Gutenberg core, but that got abandoned? |
See WordPress/gutenberg#34079.
Fixes #15.