a8cteam51 / special-projects-blocks-monorepo

MIT License
8 stars 1 forks source link

Implement Tabs block plugin #12

Open gziolo opened 4 months ago

gziolo commented 4 months ago

See https://github.com/WordPress/gutenberg/issues/34079.

Fixes https://github.com/a8cteam51/special-projects-blocks-monorepo/issues/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 commented 4 months ago

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

https://github.com/user-attachments/assets/70488ff9-9c22-4ad1-bc74-4b8489747e0d

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.

jarekmorawski commented 4 months ago

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 (https://github.com/WordPress/gutenberg/issues/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 commented 4 months ago

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 commented 4 months ago

https://github.com/a8cteam51/special-projects-blocks-monorepo/pull/12/commits/e2271caee5acc68ec3ab5f1a765aedf6b6128239 - seems to work as expected šŸ˜„

https://github.com/user-attachments/assets/4e28c6a2-3260-4c18-8e24-38381d4a39ab

michalczaplinski commented 4 months ago

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 commented 4 months ago

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 commented 4 months ago

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:

https://github.com/user-attachments/assets/16896c3e-7fc2-4ad0-bab7-9174e2471a54

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 commented 4 months ago

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 commented 4 months ago

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 commented 4 months ago

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 commented 4 months ago

I did another shortcut and copied the adjusted JavaScript implementation from ARIA Authoring Practices Guide shared in https://github.com/a8cteam51/special-projects-blocks-monorepo/pull/12#issuecomment-2238778159. This is how the entire experience looks like with keyboard interactions tracked in the screencast:

https://github.com/user-attachments/assets/a736dacb-4d02-4414-88a8-dc66aae762c1

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:

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:

jarekmorawski commented 4 months ago

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. šŸ™‡

tommusrhodus commented 1 month ago

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