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

Add initial implementation of library #2

Merged
merged 4 commits into from
Feb 10, 2024
Merged

Add initial implementation of library #2

merged 4 commits into from
Feb 10, 2024

Conversation

knuton
Copy link
Member

@knuton knuton commented Feb 1, 2024

This is the initial implementation supporting group dispatch (including recursive groups), as well as skipping and trapping.

There is limited functionality for avoiding issues with form elements, so this may have to be revisited as we gain more experience.

There is also some limited functionality for automatically handling <dialog> or other top-layer elements, but the current browser APIs don't allow to determine which is the top-layer, so one would probably have to work with trap levels if there are several dialogs. This may be refined if we end up making use of dialog elements.

Tasks

  • Update README
    • Related libs
    • Installation
    • Better example
  • Setup for npm package
  • Streamline module structure (sections, naming for clarity)

Another lib that I looked at but did not draw as much inspiration from:

@knuton knuton changed the title WIP Clean implementation Add initial implementation of library Feb 2, 2024
Squashed into one commit as large refactors were interleaved with
applying formatters with different settings during initial
stabilization, so producing a nice readable history is unfortunately
infeasible.
@knuton knuton marked this pull request as ready for review February 2, 2024 12:57
@knuton knuton requested a review from stoeffel February 2, 2024 12:57
@@ -0,0 +1,913 @@
/**
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 we should wrap the whole thing in a (function(){...})()

Copy link
Member Author

Choose a reason for hiding this comment

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

I was thinking of this as an ESM file, so I think it's not needed? I am however not completely up to date with how a UMD/CJS/ESM omni-module would look like and whether it is a desirable goal.

do {
nextParent =
(nextParent.parentElement &&
nextParent.parentElement.closest("[data-focus-group]")) ||
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 we would get the same effect if we just used body all the time, but maybe we could improve by finding the next group on the same level in the direction instead. See recording for what I mean:

Screen.Recording.2024-02-05.at.09.28.37.mov

Copy link
Member Author

Choose a reason for hiding this comment

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

By getting the same effect you mean the behaviour wouldn't change if we just always jumped to body as the container?

I wanted to just test your hypothesis, but I think I don't understand the idea fully enough to put it into code. But when you look at the third event's logs (triggered when ArrowDown was pressed while "Sub-sub-button 2" was focused, so in an actually nested group), we do not rise to the body element, and in my mental model the behaviour would be different if we did.

Copy link
Contributor

Choose a reason for hiding this comment

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

By getting the same effect you mean the behaviour wouldn't change if we just always jumped to body as the container?

yes it wouldn't change. also .closest isn't directional, so if you press ArrowUp it still find the one below if it's closer, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, .closest just goes up the DOM tree. We use it to look for focusables in the next container, then filter out any that aren't in the right direction. If there are none left, we keep going up and repeat; if there are one or more, we select the best candidate (mostly by distance).

We should never go down when pressing ArrowUp, I think. This is ruled out when candidateElements is populated with the filtered version of annotatedElements:

focus-shift/index.js

Lines 170 to 185 in ac7c0f1

const annotatedElements = getFocusableElements(nextParent).map((e) =>
annotate(direction, activeRect, e)
)
candidateElements = annotatedElements.filter(({ rect }) => {
switch (direction) {
case "left":
return Math.floor(rect.right) <= activeRect.left
case "up":
return Math.floor(rect.bottom) <= activeRect.top
case "right":
return Math.ceil(rect.left) >= activeRect.right
case "down":
return Math.ceil(rect.top) >= activeRect.bottom
}
})

Copy link
Member Author

@knuton knuton Feb 6, 2024

Choose a reason for hiding this comment

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

When applying the following diff (the loop would no longer be needed), navigating within groups no longer works properly:

diff --git a/index.js b/index.js
index 8f2c91a..87a3f4e 100644
--- a/index.js
+++ b/index.js
@@ -162,10 +162,7 @@ function getFocusCandidates(direction, activeElement, container) {
   let candidateElements = []
 
   do {
-    nextParent =
-      (nextParent.parentElement &&
-        nextParent.parentElement.closest("[data-focus-group]")) ||
-      container
+    nextParent = document.body
 
     const annotatedElements = getFocusableElements(nextParent).map((e) =>
       annotate(direction, activeRect, e)

But maybe you mean something different?

@stoeffel
Copy link
Contributor

stoeffel commented Feb 5, 2024

Pressing right when focused on the first button seems to go to a unexpected place for me:

Screen.Recording.2024-02-05.at.09.32.00.mov

@knuton
Copy link
Member Author

knuton commented Feb 5, 2024

Pressing right when focused on the first button seems to go to a unexpected place for me:

This is "by design" in how I think about it currently. Looks like I accidentally removed a note about this from the README, should probably add it back.

Currently the algorithm only consider elements that start in the intended direction as candidates, and so the groups underneath the button in your video are not considered candidates. At first I thought this was a flaw in the concept and we could change the algo to say "a candidate is any element starting in the direction of movement or containing an element starting in the direction of movement". But then I wasn't so sure that any of the groups should really be activated using a "right" movement in this example, especially as the group may have its own strategy for choosing the focused element. And this strategy may not select purely on spatial grounds. So it makes sense here to go "down" to the group, more than "right" to an element in the group.

So much for explaining why the groups are skipped, does that make sense?

Going beyond this, I agree that it is surprising to jump to that button so far in the distance. I haven't tried the "dominion" idea as it doesn't seem to be currently needed in Play's layout, but I think this could help reduce such unintuitive behaviours.

@stoeffel
Copy link
Contributor

stoeffel commented Feb 6, 2024

So much for explaining why the groups are skipped, does that make sense?

I think it's okay. I do think it's not a very likely scenario for using this anyways.

@knuton knuton merged commit da00214 into dividat:main Feb 10, 2024
6 checks passed
@knuton knuton deleted the rewrite branch February 10, 2024 12:18
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.

2 participants