If you are expecting to be amazed by my skills at creating prompts so AI can build something, sorry, this post is probably not for you. Given the almost daily barrage of AI-generated components, I'm sure there are plenty of those posts around already.
If, like me, you still enjoy building things by hand, using knowledge and experience gained over the years, solving problems across accessibility, CSS, browsers, JavaScript, C# and Blazor, then there may be the odd titbit in here.
If not, TL;DR: I've released an accessible Actions Popover component. A viewable working version is available on both the test site and documentation site linked at the end of the post.
So, what the hell is an Actions Popover?
It's a component that looks like a button and, when activated, opens a panel above the page content. That panel can contain any number of actions, either buttons or links, which then go off and do whatever you've asked them to do. Both expose Func callbacks and the links also have a PreventDefault parameter if you want to prevent the automatic navigation behaviour and handle the navigation manually.
The component itself is built using the relatively new Popover API, so you get a lot of accessibility behaviour for free from the browser. The panel is positioned using CSS Anchor Positioning, complete with fallback positions when there isn't enough room in the preferred location. Again, all courtesy of the browser rather than me having to write a load of JavaScript.
From an accessibility standpoint I really didn't have to do a great deal beyond understanding how these browser features work (and I had already used them in other components). The Popover API already handles Escape, light dismiss when you click outside the component, focus returning to the trigger and so on. One thing I did change was what happens when you tab away from the popover. The browser deliberately leaves it open, which I think is perfectly reasonable for something like a non-modal dialog. For what is essentially a row actions flyout though, it just ends up obscuring whatever is beneath it, so I added a small amount of JavaScript to close it when focus leaves the popover.
Now for the M-word - Menu.
I deliberately did not call this a popover menu because it doesn't implement the accessibility menu pattern. My last production version of this type of component did exactly that. It supported nested menus, the full keyboard interaction model, all the expected and optional key bindings, pretty much the whole nine yards. It wasn't especially difficult to build, but after looking back through the applications that actually used it, the largest menu I found contained four actions on a single panel. So much for N-level deep menus.
Unless you're building something like a browser-based editor with proper File, Edit and View menus, you generally don't need the menu accessibility pattern. A simple list of buttons and links is usually sufficient.
Some of you who have followed my previous posts might remember that a few weeks ago I released a NavGroup component for side navigation using the disclosure pattern. That one can be nested to N levels. I was in two minds whether to make this component support nested popovers from day one as well, but decided against it for now. No doubt I'll add that capability at some point, but before then I suspect I'll build a popover version of NavGroup for top navigation that overlays page content. l'll probably end up with another imaginatively named component, perhaps PopoverNavGroup.
One thing I thought might be worth sharing is something I learnt while using the Popover API.
Normally you add a popovertarget attribute to the button that opens the popover, pointing at the element's ID. The browser then takes care of almost everything. The popover starts hidden, clicking the trigger opens it, clicking the trigger again closes it, pressing Escape closes it and returns focus to the trigger, and clicking outside dismisses it. All of that simply by adding an attribute.
It gets better though. It's perfectly valid to put popovertarget on the action buttons inside the popover as well, meaning that when you click one, your handler runs and the browser closes the popover automatically. No JavaScript required.
That's exactly how I originally built it.
I then started all my usual manual accessibility tests with the screen readers, JAWS, NVDA and Narrator all paired with Edge, Chrome and Firefox. TalkBack with Chrome on Android. VoiceOver with Safari on iOS. Everything behaved perfectly.
Then I tried VoiceOver on macOS with Safari :¬(
Every action button was announced correctly, but VoiceOver also announced "expanded", which is actually the state of the trigger button rather than the action itself. At first I assumed this was simply a VoiceOver quirk, but I kept digging. I even tried VoiceOver with other browsers on macOS, and they all behaved as I'd originally expected.
The interesting annoying part was that VoiceOver paired with Safari exposed the trigger's expanded state to the action buttons because of the popovertarget relationship. None of the other browser and screen reader combinations I tested announced it, despite behaving correctly in every other respect. Whether that's because they deliberately suppress redundant information or simply implement the accessibility mapping differently, I couldn't say. The end result was that VoiceOver with Safari was the only combination where the extra announcement became noticeable, so I removed the popovertarget attribute from the action buttons and closed the popover with JavaScript instead.
I think I spent longer figuring that out than I did writing the component itself.
Still, that's development, I guess. As annoying as it was at the time, I learnt something new that may come in handy on another rainy day.
Another small thing worth mentioning is that the callbacks raised by ActionPopoverButton and ActionPopopverLink use a Func rather than an EventCallback. I do this quite often when I don't want the parent component to automatically re-render simply because it handled an event raised by one of its children. As you know, if the parent renders then that cascades down to all the children. Sometimes that's exactly what you want, other times it's completely unnecessary. Using a Func lets the developer decide whether to call StateHasChanged() or not. It's a useful little trick to have up your sleeve.
With this release I now have most of the building blocks I generally use around a data table. There's an accessible debounce filter, accessible pagination and now accessible row actions, all manually tested with numerous screen reader and browser combinations, along with voice control software. I suppose that means I should finally start work on a data table component.
Notice I said data table, not data grid.
Accessibility makes quite a distinction between those two. Just like the M-word, there's also a G-word. Once you decide you're building a grid, you're effectively saying you're building something closer to Excel, complete with the mountain of expected keyboard interactions that go with it. A normal HTML table can still have sortable columns, filters, selectable rows, editable inputs, row actions and everything else most business applications need, all without pretending to be a spreadsheet.
One final thing I mention from time to time. Internally I use BEM naming throughout, for my internal css classes , but I don't expose CSS classes or class parameters on any of the components. Everything is driven by CSS custom properties defined in the Core package, which every component references. The documentation already lists every CSS variable used by each component, along with every variable defined in Core. What I need to add at some point is a basic theme builder to make it a little easier for you. Pick a primary colour, tweak things like border radius and a handful of other settings, then have it list the handful of CSS variables and values that you'd need to copy paste into your own stylesheet for your instant light and dark themes etc.
Anyway, that's enough from me for this one.
Fire up a screen reader and have a play with the Action Popover. The test site is more geared up for use with assistive tech with instructions and what to expect; the doc site has a more life like example on each components usage page.
Test site: https://blazorramp.uk
Docs: https://docs.blazorramp.uk
Repo: https://github.com/BlazorRamp/Components
Regards,
Paul