-
Notifications
You must be signed in to change notification settings - Fork 2
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
Conversation
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.
@@ -0,0 +1,913 @@ | |||
/** |
There was a problem hiding this comment.
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(){...})()
There was a problem hiding this comment.
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]")) || |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
:
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 | |
} | |
}) |
There was a problem hiding this comment.
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?
Pressing Screen.Recording.2024-02-05.at.09.32.00.mov |
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. |
I think it's okay. I do think it's not a very likely scenario for using this anyways. |
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
Another lib that I looked at but did not draw as much inspiration from: