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

Support dynamic tabbableChildren for TableCellView #2340

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

msmithNI
Copy link
Contributor

@msmithNI msmithNI commented Aug 9, 2024

Pull Request

🀨 Rationale

#2210

πŸ‘©β€πŸ’» Implementation

Summary of changes:

  • TableCellView tabbableChildren changed from a getter to be an @observable property
  • Both anchor & menu button columns only have a tabbable/focusable element in cells in some cases, so opt them into this change.
  • Create a new dynamicRef directive, used in the anchor/ menu button cell templates. The issue with the ref directive is that once a reference is initialized, it's never cleared. So, for example, if an anchor column starts out with valid URL data then gets data without URLs, cellView.anchor would still be set (to an element no longer in the DOM) with ref, whereas dynamicRef clears out the property
    • As an alternative, I considered the children directive with a filter, but it seemed like that would require adding yet another extra container div to the cell views, which didn't seem desirable.
  • When tabbableChildren changes, the cell view fires a bubbling 'cell-tabbable-children-change' event, handled by KeyboardNavigationManager, which focuses the cell instead (if the cell content was previously focused, and is no longer present)

Implementation concerns:

  • 'cell-tabbable-children-change' event
    • This works, but feels somewhat heavy handed (but I couldn't really think of a better alternative).
    • This fires for every visible row, not just the focused row (extra events firing that won't be handled)
    • KeyboardNavigationManager keeps track of focused rows/ cells via index, whereas most of the rest of the table code uses recordId and columnId. In order to only handle the event for the focused row, there's some new code in KeyboardNavigationManager that keeps track of the columnId / recordId if cell content is focused, which complicates that class a bit more.

UX concerns:

  • In Chrome, if a focused element inside a cell is removed from the DOM, the table loses focus entirely. KeyboardNavigationManager tracks when the table gains/ loses focus, and won't programmatically set focus if the table doesn't already contain focus. So in this case (in Chrome) the user would need to manually refocus the table, at which point the cell would get focused. I discovered this fairly recently so haven't spent too much time pondering what it would take to have the keyboard nav code handle this specific case.
    • This didn't occur in Firefox / WebKit, which is why the new test only conditionally refocuses the table right now.

πŸ§ͺ Testing

  • Existing keyboard nav / tabbableChildren tests still pass
  • Add new test ensuring that if tabbableChildren are changed (removed), focus reverts to the cell
  • No tests for dynamicRef currently (we'd want some if we went with this approach)

βœ… Checklist

N/A for now since this draft PR is to get feedback on this approach.

@msmithNI
Copy link
Contributor Author

msmithNI commented Aug 9, 2024

<moved to thread>

@@ -0,0 +1,7 @@
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@msmithNI: @jattasNI @rajsite @atmgrifter00 interested in your thoughts on this implementation direction so far (but note the concerns I listed in the PR description).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The general direction looks reasonable to me.

For the cell-tabbable-children-change I'm a bit curious how often that event is firing. Is it firing once per row continuously as you scroll for example? That could potentially be excessive object creation / gc pressure on scroll.

In Chrome, if a focused element inside a cell is removed from the DOM, the table loses focus entirely.

Do you have a screencap of how that manifests? Not exactly clear how to try out the UX for that situation and if it's a new change in behavior or existing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No objections to the overall direction from me. (I left a couple comments while wrapping my head around the PR because I couldn't stop myself; neither one is existential, just normal code review feedback)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When cell-tabbable-children-change fires:

  • With what's in this PR, there's some unnecessary firing as the table loads with initial data. If we omit firing when !cellView.$fastController.isConnected, we could prevent that though.
  • During scrolling:
    • Anchor column: Yes, depending on what's in the cells. With the Storybook example, yes, since the recycled cells change between having valid links / no link / placeholder text, as you scroll (depending on the data).
    • Menu button: Yes with this current PR. But not if we skip firing when the FAST controller is not connected.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a screencap of how that manifests?

Not currently, I was just going by the behavior of the new autotest failing in Chrome without the code to refocus the table. If we updated an example to periodically update the table data with an anchor column periodically switching between a valid URL / no URL, that would probably show it.

@@ -1422,6 +1426,28 @@ describe('Table keyboard navigation', () => {
expect(blurSpy).toHaveBeenCalledTimes(1);
expect(currentFocusedElement()).not.toBe(cellContent);
});

it('and then the cell updates to no longer have tabbableChildren, the cell is focused instead', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I presume if we went with this approach (which I think I'm fine with), that there are some other tests we still need to add, yes? Namely, that after the tabbableChildren change (but not completely removed, as this test covers that) that tabbing (and shift-tabbing) behave as expected.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, those would be good tests to add too.

@@ -0,0 +1,62 @@
import {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does FAST have unit tests for their RefBehavior that we could leverage for patterns to write unit tests for this class?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that I've found, but writing our own should be pretty quick anyway.

Comment on lines +348 to +355
if (
this.focusType === TableFocusType.cellContent
&& cellView.recordId === this.cellContentState.recordId
&& cellView?.column?.columnId === this.cellContentState.columnId
) {
if (
this.cellContentState.index >= cellView.tabbableChildren.length
) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you could collapse these two ifs into one.

@@ -0,0 +1,7 @@
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No objections to the overall direction from me. (I left a couple comments while wrapping my head around the PR because I couldn't stop myself; neither one is existential, just normal code review feedback)

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

Successfully merging this pull request may close these issues.

4 participants