w3c / html-aria

ARIA in HTML
https://w3c.github.io/html-aria/
Other
178 stars 48 forks source link

a with href should allow row, cell, and gridcell roles #473

Open devongovett opened 1 year ago

devongovett commented 1 year ago

Currently the allowed roles for an <a> element with an href attribute are: button, checkbox, menuitem, menuitemcheckbox, menuitemradio, option, radio, switch, tab or treeitem. I think row, cell, and gridcell should be added to this list. It is very common to build tables, lists, cards, tags, and other UI controls using the ARIA grid pattern. Often times, the rows or cells in these patterns need to be clickable to perform a navigation to a different page, for example, a detail page for an object in a table row. While these roles allow placing a link inside the cells (unlike the currently listed rows), clicking on the entire row is often desirable to give users a larger hit target.

We can perform navigation via JavaScript when clicking on a row, but this means that native browser behavior such as opening in a new tab is not available. It would be nice to be able to simply use <a role="row" href="..."> instead of relying on JavaScript. This already seems to work fine with screen readers in my testing, but due to the spec restriction is flagged by automated accessibility checkers. The role override on the <a> element means that nested content is accessible, unlike a regular link. The only difference from a <div role="row"> or a <tr> is that the browser default click behavior works as expected rather than needing JavaScript. Therefore I do not see a downside to allowing this.

scottaohara commented 1 year ago

can see the argument that cell/gridcell could be additions to the allowed roles for hyperlinks. Similar updates were made for button elements. the extra inherit behavior of hyperlinks of course being different than button.... so it still seems like something to think about a bit more.

The use case for role=row makes sense if you are only looking at this only from a "what does a screen reader expose or not" pov. there are other behaviors inherit to hyperlinks that would make this problematic - e.g., i can't use the mouse to click/select text anymore where that could be handled better with JS - the potential HTML validation issues that would occur with devs thinking it's fine to make a hyperlink into a row, so then they can nest other interactive elements inside that link that's now exposed as row but still behaves like a link.

stepping back a bit on this works fine with screen readers bit as well, but someone navigating with table commands would not be navigating by rows, so a second hyperlink would likely need to be available in one of the cells to expose the fact there is a link to activate. So that again would create a situation where someone would have a hyperlink wrapper where its role was suppressed by the role=row, and then need another hyperlink within that to allow virtual cursor access to the link? that seems messy.

but happy to understand this ask further and how the concerns i mentioned could be addressed - ideally in a way that wouldn't involve as much work/more work than with what can be done with JS now without needing this change.

devongovett commented 1 year ago

Thanks for your thoughts! A few responses:

there are other behaviors inherit to hyperlinks that would make this problematic - e.g., i can't use the mouse to click/select text anymore

Yeah that's true, but I don't think it's any different from the other roles that are allowed on an <a href>. For example, a link with role="option" or role="menuitem" wouldn't be selectable by default either. I think this is expected if you want to make the entire row clickable. It is a very common experience in many applications. For example in Gmail, rows in the table are clickable to navigate to a message. Given that this is opt-in (by using an <a> element for the row/cell), the developer can make a UX choice about how it should work based on their context. If selection of content is important, then they could easily place a link inside a cell instead of turning the whole row into a link.

the potential HTML validation issues that would occur with devs thinking it's fine to make a hyperlink into a row, so then they can nest other interactive elements inside that link that's now exposed as row but still behaves like a link.

Yeah I think this validation rule makes sense in most cases, when there is not a role override. However, when the link has a role that does allow interactive children, the semantics of that role should apply. This is already the case in current implementations, so I believe the validation rules probably should be clarified to reflect that as well.

Cases like this come up all the time in real world applications, and I know it's a frustration for many developers. There are many articles discussing various techniques to achieve clickable rows and cards using hacks like extending the click area of an element, or re-implementing browser behavior (often poorly) in JavaScript. What I'm suggesting here is to expand what is allowed by the spec so that native link behavior can be reused rather than implementing something equivalent with JavaScript just to avoid validation errors, and missing out on default behavior like command/control clicking to open in a new tab.

so a second hyperlink would likely need to be available in one of the cells to expose the fact there is a link to activate

Yeah, this is how gmail implements this today. The whole row is clickable but the subject column is also announced as a link. It's also how card patterns are often implemented – the title of the card is a link, and a JavaScript listener is installed at the entire card level to trigger that link when a user clicks anywhere inside the card. With what I'm suggesting, you could implement the same without JS.

scottaohara commented 1 year ago

Yeah that's true, but I don't think it's any different from the other roles that are allowed on an <a href>

Hmm, then we're looking at this very differently then and I'd submit a row of tabular data, which often contains multiple strings of text and potentially other nested interactive elements, is quite different than a single option or menuitem that contains a concise string of text.

The comparison to gmail is an interesting one, but not as an example that is very compelling for what you're requesting. They have implemented this not using wrapping hyperlinks (as you are already aware) - which makes sense because they have nested interactive element for checking, marking the message as important, and multiple other actions. They aren't even using an actual a href to represent the "link" within their table, which makes sense in their context because they don't actually want the extra implicit behaviors of a hyperlink (e.g., all the right-click context menu items that appear - nor is there likely any benefit for them to insert the generated URL fragment into the href attribute.

the developer can make a UX choice about how it should work based on their context. If selection of content is important, then they could easily place a link inside a cell instead of turning the whole row into a link.

not really though, because as mentioned the developer would need to put the nested link inside of the link anyway for someone navigating by virtual cursor. So while you were mentioning that in context to text selection, it would still be a necessary requirement to also declare a link within one of the cells of the row. One could get around the nested hyperlink issue by using a div role=link within the cell. But one would likely need to use JS to make that link functional when focused directly on it...so, would still need js then.

... With what I'm suggesting, you could implement the same without JS.

This is not true though. Since adding a role does not negate any of the functionality of the hyperlink, you'd still run into event bubbling with the nested interactive elements within the link. E.g., a button or other hyperlink nested within the parent - clicking on one of those will cause the action to perform as well as the parent hyperlink being activated. This would just mean that developers wouldn't need to use JS to make the whole row clickable, but then they'd just need to write different JS to suppress the implicit behavior of the hyperlink/row - not to mention the fact that the row likely shouldn't be in the tab order in most cases (or even focusable using arrow keys - navigating by cells is generally preferred and I'm privy to various bugs that come up with produt teams where the row is focusble instead, as it is not generally a good UX for screen reader users). And then there's the unstyling of the native hyperlink styles as well. All those together being likely small tasks, but also all new points of failure for developers. In an effort to not use JS, JS is still needed/likely to be used anyway.

This is one of the reasons why Adrian and Heydon's articles don't do similar to what you're suggesting and instead use other methods to make the entire component clickable.

Unfortunately, this use case/response makes me feel more strongly that for most use cases - and particularly for even complex use cases where the implementation is going to heavily rely on JS anyway - this wouldn't be a net positive change. It'd instead require devs just do a variety of different sub-tasks.

===

I think there are probably use cases to allow gridcell on a hyperlink, matching the updated allowance for button element which will land in #446 - which would generally be more inline with your comparison to option and menuitem.

devongovett commented 1 year ago

I guess the question here is whether the HTML validator should be making UX decisions when ARIA, CSS, and JavaScript can completely override the behavior and semantics of the element. Perhaps I phrased this poorly before: yes you'll still need JavaScript. That's a requirement for most ARIA stuff. In this case, you'll need to stop propagation on click events for nested elements so they don't reach the link and perform a navigation.

To be clear – all of the things you have raised are excellent points, but they all would need to be solved with a purely JavaScript-based solution as well. You'd still need to stop propagation so the row doesn't navigate when clicking on nested interactive elements. You'd still need to handle text selection somehow, or decide that it's not important in your context. You'd still need to place a link in one of the cells for screen readers. People are already doing all of this today.

The one thing an HTML-based solution can provide that a JS-based one cannot is full support for all of the builtin browser behavior for links. With a JS solution, you cannot command click on a row to open in a new tab, alt click to download a linked resource, or right click to perform actions like adding to bookmarks, etc. Being able to click on the whole row to navigate is convenient, but not having it behave like a normal link in all of these other ways is a UX problem.

So to clarify, I think for these cases the implementation of row-level navigation would be almost identical between a <div role="row"> with a click handler to programmatically navigate to a URL, and with a <a role="row">. The difference would be that the browser would provide the navigation instead of doing it programmatically, along with providing other link behaviors that JS cannot provide.

Again, this already works fine behaviorally, it simply a spec-level/validator-level restriction that causes failures in automated tools. Yes you have to deal with the issues you've raised, but you'd need to do that regardless of which element you use. Therefore, I'm looking for a way to get the full benefits of using a <a> element but without failing in automated validation tools.

scottaohara commented 1 year ago

i'm worried that my concerns / the behavioral issues i'm thinking of aren't coming across in the way that i would hope. as your response of "this already works fine behaviorally" seems to indicate to me that we're speaking past each other, as I'm not getting a sense from your responses that my concerns are being taken into account. If that's my fault for not being specific enough, I apologize. But maybe this method of discussion isn't the best, as each of my responses thus far have already taken me a considerable amount of time to write, and this doesn't seem to be helping.

Specifically you mention "...ARIA, CSS, and JavaScript can completely override the behavior and semantics of the element" as a reason that a validator should allow this - but you are also purposefully trying to do this so as to not override the implicit behaviors of the hyperlink - just its role semantics. if one were to actually overwrite everything, then you've done a bunch of work to create a div, and then sure... no real issue there. that's not what's actually being proposed here, and thus it just brings us back to some of the issues i've tried to mention previously.

but, if not having to use JS was the actual end goal here (though it's clear that that's not what is really being asked, since JS would still need to be involved anyway, for most cases) - why not take a page from one of the referenced articles?

https://codepen.io/scottohara/pen/PoxmXmJ?editors=1100

if you're wanting to use a hyperlink as a row - then you're already not using a native HTML table in the first place. So one could ensure their table rows could be marked up and styled in a way where the pseudo element of the hyperlink could extend over the entire area of the row. Then, any nested text or interactive elements could be positioned atop that pseudo element, allowing for a good majority of the row to function as you are requesting - without the need to ensure that a hyperlink exists within the "row" hyperlink so that someone using a screen reader could activate it / someone using voice dictation software could access it, etc.

so re:

I'm looking for a way to get the full benefits of using a <a> element but without failing in automated validation tools.

what's the problem with doing that (the above mentioned pseudo element solution)? it meets your goal and mitigates all the concerns with what you're asking to do instead.

Note: i realize a simple test case is likely not going to take all of one's actual concerns/use cases in to account. so there may very well be a reason as to why you could not simply use the quick demo i created - but i'd be curious as to where this falls down, and what might need to be modified, and if that's actually more/less work than the effort that'd be involved for similar use cases per the changed allowances you're proposing.

devongovett commented 1 year ago

as I'm not getting a sense from your responses that my concerns are being taken into account

I'm trying to say that they are being taken into account already. These are issues regardless of whether you use a native hyperlink or implement it from scratch with a div, and there are ways around them as I mentioned (such as stopping propagation on nested interactive elements). I really do appreciate you taking the time to respond to this, and thinking about all of these issues. I apologize if it seems like I'm not hearing you. I don't mean it to come across that way. These are valid concerns and I'm glad you've raised them.

Let me back up and explain where I'm coming from. I work on a component library, which includes a Table component. We follow the ARIA grid pattern. It's built with divs already for various reasons, such as needing to support scroll virtualization and more layout control than CSS tables support. We also have a feature to support row level actions already. This is implemented with JavaScript. We ensure that nested interactive elements do not trigger these row actions by preventing propagation of events to the row. This is working well, and consumers of our component can implement navigation programmatically. However customers have noticed that they cannot open links in a new tab or perform other native link behaviors. So we tried switching the div we have for the row to a <a> element. Given that we already handle all of the interactions for the div implementation, everything seemed to work ok in our case except that validation failed.

The problem with the pseudo element solution is that it depends on controlling the contents and styling of the row and cell. In our component library, we don't control what content applications decide to place inside the cells, nor the styling of those contents. That's all controlled by the application rather than the library, so it would be very difficult for us to implement this in a generic way that could be used across many different apps using our component. In addition, even if we could, these kind of hacks can be brittle and break in subtle ways depending on the scenario so I was hoping for a more straightforward approach.

The reason I opened this issue is because it seemed like it should be possible to rely on the native HTML behavior as long as the ARIA role was overridden appropriately, and other behaviors were put in place with JavaScript to ensure it worked properly. I can see that there are strong opinions about this. The concerns are valid – you would need to ensure that you handle a lot of behaviors so that it works correctly, and there are opportunities for misuse. However, that's the case with all ARIA. As always, it is a contract that requires developers to implement additional behaviors to ensure it works correctly. I am hoping we can come to some common understanding here.

scottaohara commented 1 year ago

I'm trying to say that they are being taken into account already.

I realize what you're saying, but you've also acknowledged in your responses that to take care of some of my concerns it would result in someone needing to write invalid markup (interactive HTML elements within hyperlinks) or specific use of ARIA on non-interactive HTML elements to work around the HTML validation and re-implement those features with JS.

I'm not going to retread this aspect of the conversation again.

I appreciate your response to the pseudo element approach. Making components where you cannot be in control of what content authors stick into them is a rather tricky situation and I appreciate the level of effort and thought that goes into building such components. I also know how not being in total control of the content can make decisions like this even more important to not take lightly, due to the high probability of misuse - regardless of the original author's diligence.

re: 'strong opinions', I would submit I'm trying to stay as objective here as possible, and thinking of the potential ramifications of what a change like this could "allow", where presently there is at least this very basic guardrail. For instance, there is an existing issue that is asking the exact opposite of what you're proposing here https://github.com/w3c/html-aria/issues/187 - I would ask you to please reconsider your assumption that this is an opinionated debate. For instance, I have purposefully not mentioned the UX problems with making an entire row clickable. That would be UX-focused and out of scope of this discussion. It's a hard line to toe, to have people who think it was a mistake to allow roles on hyperlinks at all, and also those that want to allow hyperlinks to have roles that are meant to be treated as container elements that are not generally expected to be interactive.

If this is to continue, I think the next step would be for you to provide an example of what it is you're attempting to do. Re: "These are issues regardless of whether you use a native hyperlink or implement it from scratch with a div" this specifically seems to tell me that there is something being missed here, but maybe I'm simply not understanding your point. So, help me understand you by showing me.

devongovett commented 11 months ago

Sorry for the delay in response here. I was on vacation and then lost track of this issue.

you've also acknowledged in your responses that to take care of some of my concerns it would result in someone needing to write invalid markup (interactive HTML elements within hyperlinks)

Yes I realize it is currently invalid. I guess I'm also proposing that it should not be invalid if there is a role override, and that role does allow interactive children. But I can raise this issue in the HTML spec if that should be discussed elsewhere.

I've made a basic example of what I'm trying to accomplish. It contains two versions: the first one uses the currently invalid markup to render each row as a native link using an <a> element. The second version uses JavaScript to perform navigation when a row is clicked. Both include JavaScript to prevent clicking on the nested button within one of the cells from triggering a navigation. Otherwise you can click anywhere in the row to navigate. Obviously a real implementation would be much more complex to implement the grid keyboard navigation pattern etc., but hopefully this gives you an idea. (Note that actually navigating to another domain isn't allowed by codepen, so clicking may appear to do nothing) https://codepen.io/devongovett/pen/XWyygOg

Both versions are very similar in terms of both their markup and the associated JavaScript. The only real differences are allowing the browser to handle navigation natively vs setting window.location in JavaScript. The benefit of the native version is that you get command/control click to open in a new tab, and right click to perform actions like downloading and bookmarking. These features cannot easily be emulated in JavaScript.

The first version is technically invalid, both in terms of the html-aria spec (role="row" isn't allowed on an <a> element), and in terms of HTML itself (interactive elements e.g. buttons aren't allowed inside <a> elements). What I'm proposing is that role="row" should be allowed in order to make this use case easier to achieve, and subsequently that HTML should allow interactive elements within <a> elements if they have a role override that allows interactive children. Applying a role override comes with the expectation that developers should also include corresponding JavaScript such as I've included here to ensure the interactions work as expected. I think this is in line with the expectation for any ARIA.

Does this help clarify?

scottaohara commented 11 months ago

Thanks for the demo, this is what I had expected you were trying to create - and it also demonstrates the gaps I had been mentioning.

The native navigation example doesn't contain links to either google or apple for someone using a screen reader to discover - since you've overwritten the hyperlink role with the row. Yes, if they are using the Tab key they can navigate to that row, but being that it's exposed as a row there's no indication that it should be actionable at all. If anything, it comes across as an unnecessary tab stop where now all the content of the row becomes it's accessible name "google july 12 delete"

Similarly with the JavaScript example - which i would assume you're already aware of - without an actual hyperlink being present someone using keyboard/AT wouldn't be able to use this feature. In both cases the hyperlink needs to be there to properly expose its existence, so that all users can actually interact with them. This was the driver behind why I had even suggested the pseudo element example above. If you are allowing people to create these rows without including an actual hyperlink to the content the row is linking to, then there will always be a gap here. But if you require that someone identify / include the hyperlink in the row that they want the click even on the row to link to, then again you do not need to do what you're asking. Even without using the pseudo element to make the whole row clickable - that could still be done with JavaScript and the hyperlink itself could be accessed / right clicked by users to get the associated context menu.

devongovett commented 11 months ago

The same is true of any role override of an <a> element though, including ones that are already allowed. Here's an example with links as menu items, listbox options, and tabs. They announce as those roles, and there's no indication that they are links. https://codepen.io/devongovett/pen/poQqPeV

As for the keyboard navigation, the tab key probably wouldn't be used here in a real example that implemented grid keyboard navigation. In our implementation, users can access both rows and cells via the arrow keys and there aren't any extra tab stops. Clicking while focused on a cell also works because the event bubbles to the row.

Perhaps some extra label or description could be added to help the user understand that a row is actionable, but rows often are actionable already (e.g. if they are selectable). So without further testing, I wouldn't be sure whether this is helpful or unnecessarily verbose for the user.

scottaohara commented 11 months ago

The same is true of any role override of an <a> element though...

yes... but in those cases at least the element is still being exposed as one which is implicitly expected to be interactive. i thought I had already discussed this - that a row is not a role that would convey any sort of interactivity by default - but i'm assuming that was part of a reply i ended up cutting down, as I've heavily edited a lot of my replies to not go off into what could be considered tangential rabbit holes... for instance, like mentioning using role=tab or option on a hyperlink - where the hyperlink would still perform as a hyperlink in a context where that wouldn't be expected, would create other usability/wcag issues. These are allowed roles though under the assumption that an author undoes the implicit functionality of a hyperlink. You're advocating for not doing that though. And we're back to the start of this thread again.

As for the keyboard navigation, the tab key probably wouldn't be used here in a real example that implemented grid keyboard navigation. In our implementation, users can access both rows and cells via the arrow keys and there aren't any extra tab stops. Clicking while focused on a cell also works because the event bubbles to the row.

this misses the point that it's just not necessary/expected to navigate to a row at all in many cases - but at least in the cases where it should be supported - e.g., selecting an entire row, there's a property that can be exposed to indicate why someone has navigated to the row (e.g., it's selection -though in reality it is not consistently exposed).

Perhaps some extra label or description could be added to help the user understand that a row is actionable, but rows often are actionable already (e.g. if they are selectable). So without further testing, I wouldn't be sure whether this is helpful or unnecessarily verbose for the user.

You'd definitely have to do something like that, making the rows more verbose. But in regard to further testing, it'd also be good to do that for your proposed pattern in general. For instance, using iOS VO swiping only allows someone to navigate by cells, not rows. So without a nested hyperlink one can't reach the row/know that a hyperlink was available - they'd have to tap/hard press to get that to show up. just a double tap doesn't work.

devongovett commented 11 months ago

I discussed this offline with @jnurthen. It seems like step 1 here would be to potentially add an attribute to indicate that a row is interactive. I get that rows are not usually interactive in traditional tables, but the grid role is widely used for lots of different patterns such as advanced data tables, card grids, tag components, lists with interactive children, etc. Perhaps that's not ideal, but it's the only option we currently have when building components that need to have interactive descendants. So starting with an attribute that exposes rows themselves as interactive would probably be good. @jnurthen suggested looking at https://github.com/w3c/aria/issues/746, or perhaps this could be inferred from a tabindex. We can take that discussion to the ARIA repo. Once rows can be interactive, then it might make sense to revisit allowing them to be links.

Thanks again for discussing this with me! I really appreciate it.