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 popover capabilities to SelectNext listbox #1540

Merged
merged 1 commit into from
May 10, 2024
Merged

Conversation

acelaya
Copy link
Contributor

@acelaya acelaya commented Apr 30, 2024

Depends on #1548

Add popover attribute to the SelectNext listbox, and open it via element.togglePopover() when the browser supports it.

For browsers not supporting the native popover, we continue to handle it as before.

TODO

  • Test with a screen reader, making sure the use of [popover] does not have any side effects on how components are announced.
  • Add/fix tests
  • Update documentation mentioning the popover behavior of the listbox in browsers that support it.

Considerations

  • Adding the popover attribute to the listbox makes it render in the top layer, and therefore, it is no longer positioned relative to the toggle button.
    Because of that, we have to manually calculate where should it be based on some heuristics, and absolutely position it there.
  • Related with the point above, and because of how useArrowKeyNavigation internally works, I had to disable the automatic option focusing that useArrowKeyNavigation provides, and add an extra toggle event listener that mimics that logic.
    EDIT: This has been solved. See Add popover capabilities to SelectNext listbox #1540 (comment)
  • Another side effect of rendering the listbox in the top layer, is that it won't be affected by scrolling on inner elements.
    EDIT: This is now mitigated via listboxAsPopover prop. With that we can keep the listbox as is, and enable the "popover mode" only when really needed.
    EDIT 2: Finally we are capturing the scroll event and re-adjust the listbox positioning dynamically.
  • At some point, I considered creating a new Popover component, that can be used in other places, but this is complex enough, without having to think in more abstractions. I will delay that to after this PR has been merged, and we have at least other use case for a popover.

@acelaya acelaya force-pushed the select-next-popover branch 2 times, most recently from 69b1620 to 4f9e4bf Compare April 30, 2024 14:29
@acelaya acelaya force-pushed the select-next-popover branch from 4f9e4bf to c77948c Compare May 7, 2024 13:32
@acelaya acelaya force-pushed the select-next-popover branch 6 times, most recently from 8714ed3 to 689fa55 Compare May 8, 2024 09:26
@robertknight
Copy link
Member

Adding the popover attribute to the listbox makes it render in the top layer, and therefore, it is no longer positioned relative to the toggle button.
Because of that, we have to manually calculate where should it be based on some heuristics, and absolutely position it there.

I think this is fine. In future you could use CSS anchor positioning for this. That is an extremely recent addition to the web platform, so not viable yet.

Without this, the option gets focused first, when the listbox is still positioned at the very top of the page, making the page scroll up. Then the listbox gets repositioned.

My understanding is that after the listbox is opened, if autofocus is enabled in the useArrowKeyNavigation hook, the sequence of events is:

  1. The SelectNext component's layout effect runs and schedules a state update for the position values
  2. The useArrowKeyNavigation hook's effect runs and autofocuses its preferred element
  3. The SelectNext component is re-rendered. This applies the position value update.

So the problem here is the ordering of steps (2) and (3).

If step (1) updated the DOM style synchronously, then the listbox would have its correct position by the time useArrowKeyNavigation runs.

At some point, I considered creating a new Popover component, that can be used in other places, but this is complex enough, without having to think in more abstractions. I will delay that to after this PR has been merged, and we have at least other use case for a popover.

Yes, that makes sense.

@robertknight
Copy link
Member

One other thing that occurred to me is that you could try to make the popover and non-popover use cases more similar by rendering the listbox children into their own element which is positioned at the top-level of the DOM, like how React portals work. The downside is that event bubbling won't work, so you'd either have to emulate it, or warn consumers about this hazard.

@acelaya acelaya force-pushed the select-next-popover branch from 689fa55 to 781ca90 Compare May 8, 2024 09:47
@acelaya
Copy link
Contributor Author

acelaya commented May 8, 2024

My understanding is that after the listbox is opened, if autofocus is enabled in the useArrowKeyNavigation hook, the sequence of events is:

  1. The SelectNext component's layout effect runs and schedules a state update for the position values
  2. The useArrowKeyNavigation hook's effect runs and autofocuses its preferred element
  3. The SelectNext component is re-rendered. This applies the position value update.

So the problem here is the ordering of steps (2) and (3).

If step (1) updated the DOM style synchronously, then the listbox would have its correct position by the time useArrowKeyNavigation runs.

You are right! If the styles are directly set via listboxElement.style.{prop} = ... instead of setting via the style={positioningStyle} prop with a piece of state, then there's no need to re-render and things work in the right order.

@acelaya acelaya force-pushed the select-next-popover branch 10 times, most recently from 3303ad3 to ce84fe8 Compare May 9, 2024 10:03
Copy link

codecov bot commented May 9, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 100.00%. Comparing base (f1f477b) to head (af4a1c2).

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #1540   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           63        63           
  Lines         1008      1041   +33     
  Branches       383       395   +12     
=========================================
+ Hits          1008      1041   +33     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@acelaya acelaya force-pushed the select-next-popover branch from ce84fe8 to 643696e Compare May 9, 2024 10:14
@acelaya acelaya force-pushed the select-next-popover branch 3 times, most recently from e81cd0c to 538b24a Compare May 9, 2024 12:24
@acelaya acelaya marked this pull request as ready for review May 9, 2024 12:26
@acelaya acelaya requested a review from robertknight May 9, 2024 12:26
@acelaya
Copy link
Contributor Author

acelaya commented May 9, 2024

The only uncovered line that codecov is complaining about, is marked as istanbul ignore next.

@acelaya acelaya force-pushed the select-next-popover branch from 538b24a to 45b2435 Compare May 9, 2024 13:31
@acelaya acelaya changed the base branch from main to frontend-testing-lib May 9, 2024 13:31
@acelaya acelaya force-pushed the select-next-popover branch from 45b2435 to d78c20e Compare May 9, 2024 13:32
Copy link
Member

@robertknight robertknight left a comment

Choose a reason for hiding this comment

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

The implementation generally looks good and worked as expected in all browsers that I tested. The main feedback I have is to add some comments for some of the more subtle issues in the code: why the listbox is being positioned manually, why the styles are applied synchronously, why we're using a popover in the first place. You don't need to repeat everything in the PR description. Just brief notes to raise awareness will do.

src/components/input/SelectNext.tsx Show resolved Hide resolved
src/components/input/SelectNext.tsx Outdated Show resolved Hide resolved
src/components/input/SelectNext.tsx Show resolved Hide resolved
src/components/input/SelectNext.tsx Show resolved Hide resolved
src/components/input/SelectNext.tsx Outdated Show resolved Hide resolved
src/components/input/SelectNext.tsx Outdated Show resolved Hide resolved
src/components/input/test/SelectNext-test.js Outdated Show resolved Hide resolved
Base automatically changed from frontend-testing-lib to main May 9, 2024 14:25
@acelaya acelaya force-pushed the select-next-popover branch 5 times, most recently from 4ac5b64 to b25768d Compare May 10, 2024 08:15
@acelaya acelaya requested a review from robertknight May 10, 2024 08:15
@acelaya acelaya force-pushed the select-next-popover branch from b25768d to af4a1c2 Compare May 10, 2024 08:19
@@ -21,13 +21,26 @@ describe('SelectNext', () => {
* Whether to renders SelectNext.Option children with callback notation.
* Used primarily to test and cover both branches.
* Defaults to true.
* @param {boolean} [options.defaultListboxAsPopover] -
* Whether we should let the `listboxAsPopover` prop use default value
* if not explicitly provided, or we should initialize it instead.
Copy link
Member

Choose a reason for hiding this comment

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

Assuming most tests don't set this option, it means we'll be explicitly disabling the popover usage, right? We probably want tests to use the default behavior of the component except for tests that are specifically opting out.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, I agree. However, there are some tests failing when using the popover mode, mostly around testing that focusing works as expected in certain scenarios (first option gets focused when opened, the toggle recovers focus when closed, etc.). I assume it's related with the use of the top layer, but I couldn't confirm it.

I tried a couple of times to make them pass, but finally decided to disable the popover mode by default for the sake of getting existing tests to pass, and focus on popover mode only for new tests.

I will address this in a follow-up PR, as I think you are definitely right, but I wanted to offload some indirect complexity from this one.

Copy link
Member

@robertknight robertknight left a comment

Choose a reason for hiding this comment

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

The added comments LGTM. I did have a minor query about the default value of listboxAsPopover in tests. It looks like the default is to turn this behavior off except for tests that opt in. Is that intended?

@acelaya acelaya merged commit 639bfc7 into main May 10, 2024
4 checks passed
@acelaya acelaya deleted the select-next-popover branch May 10, 2024 09:10
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