Skip to content
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

Draft
wants to merge 17 commits into
base: trunk
Choose a base branch
from
Draft

Implement Tabs block plugin #12

wants to merge 17 commits into from

Conversation

gziolo
Copy link
Collaborator

@gziolo gziolo commented Jul 15, 2024

See WordPress/gutenberg#34079.

Fixes #15.

A block that allows users to organize content into tabs. I’m not sure about all the UX yet (the relationship between the actual tab element and its content is tricky to get right in the editor), but it could be a good starting point.

tabs

@gziolo
Copy link
Collaborator Author

gziolo commented Jul 16, 2024

I was able to implement basic interactions for tabs in the editor:

July.16th.progress.report.mov

I'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.

@gziolo gziolo requested a review from michalczaplinski July 16, 2024 14:38
@gziolo gziolo added the enhancement New feature or request label Jul 16, 2024
@jarekmorawski
Copy link

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.

@gziolo
Copy link
Collaborator Author

gziolo commented Jul 17, 2024

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.

That's the missing part I left as the last task, as it requires more tinkering. At the moment, I have in place all the functionalities in place to manage tabs from the List View and to add more tabs from the sidebar:

Screenshot 2024-07-17 at 13 52 32

It always shows only the content for the actively selected tab. So when you select any block from a different tab, it switches to what's visible. It's going to be more intuitive when I add the actual UI for switching the tabs in the Tabs block UI shown in the editor's canvas.

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.

It's more nuanced and if I recall correctly it's also why we ended up offering the setup screen for Columns, Group, or Table blocks. In particular, when you initialize the editor, you can't recognize whether the block was just inserted or it was loaded again with no inner blocks. So the placeholder state ensures the behavior is consistent. One way to go about it though, would be to offer the default variation that always has 2 tabs. Let me quickly prototype it.

@gziolo
Copy link
Collaborator Author

gziolo commented Jul 17, 2024

e2271ca - seems to work as expected 😄

Progress.July.17th.mov

@michalczaplinski
Copy link
Collaborator

In particular, when you initialize the editor, you can't recognize whether the block was just inserted or it was loaded again with no inner blocks. So the placeholder state ensures the behavior is consistent.

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.

@jarekmorawski
Copy link

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 Tab blocks inside.

@gziolo
Copy link
Collaborator Author

gziolo commented Jul 18, 2024

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 Tab blocks inside.

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.mov

In 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.

@jarekmorawski
Copy link

jarekmorawski commented Jul 19, 2024

Good progress! Love to see to the block take shape.

In 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.

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.

@gziolo
Copy link
Collaborator Author

gziolo commented Jul 19, 2024

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.

@jarekmorawski
Copy link

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.

→ ← – navigate to tab, press again to activate it, and focus on the title. Press again to move horizontally, deselect the tab, and focus on the next one.
↑ ↓ – with the tab open, navigate to the content and jump between inner blocks exactly like in the Group block.

@gziolo
Copy link
Collaborator Author

gziolo commented Jul 19, 2024

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.mov

As 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:

  • Use Interactivity API instead of vanilla JavaScript.
  • Audit the default HTML and CSS code produced.
  • Decide whether using static blocks is the most efficient approach, as there are some nuances to keep in mind. In particular, ensuring that every individual Tab block has proper state reflected in the HTML attributes required using useEffect in the edit component to sync the current tab number and whether it is the one that is going to be active on the frontend on initial load.
  • Figure out the best method for generating unique IDs for tabs and panels. At the moment, it won't work correctly with two or more Tab blocks on the same page.

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:

  • Should the first tab always be the default one?
  • What is the most reliable way to set the default active tab when loading the page?
  • How can interaction between the list of tabs be improved? @jarekmorawski shared some thoughts in Implement Tabs block plugin #12 (comment).
  • As noted in Implement Tabs block plugin #12 (comment) by @jarekmorawski, we need to improve in WordPress core a way to define the starting set of inner block when inserting the Tabs block. Related to that, if we want to remove the placeholder state, how do we prevent removing all tabs or what happens when the last tab gets removed?

@jarekmorawski
Copy link

Should the first tab always be the default one?

I'd say yes. It makes the editing experience kind of self-explanatory.

What is the most reliable way to set the default active tab when loading the page?

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. 🙇

@dhanson-wp dhanson-wp linked an issue Jul 19, 2024 that may be closed by this pull request
@tommusrhodus
Copy link
Contributor

Looks like this was going to head over to Gutenberg core, but that got abandoned?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

Tabs Block
4 participants