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

Improve control over Menu and Listbox options while searching #2471

Merged
merged 9 commits into from May 4, 2023

Conversation

RobinMalfait
Copy link
Collaborator

@RobinMalfait RobinMalfait commented May 2, 2023

This PR improves the control over the "text value" of a Menu.Item or Listbox.Option.

When a Menu or a Listbox is in an open state, then you can just start typing to search for a specific item. It uses a simple algorithm where it looks at the beginning of the option and if it matches your search or not. If you are on an option and you re-type the same thing, then it will jump to the next option that matches.

Concretely, let's look at this setup:

<Listbox>
  <Listbox.Button>Country</Listbox.Button>
  <Listbox.Options>
    <Listbox.Option value="us">United States of America</Listbox.Option>
    <Listbox.Option value="ca">Canada</Listbox.Option>
    <Listbox.Option value="be">Belgium</Listbox.Option>
    <Listbox.Option value="by">Belarus</Listbox.Option>
  </Listbox.Options>
</Listbox>

If you open the Listbox, then type:

  • united then it will jump to the United States of America
  • bel then it will jump to the Belgium
  • bel (again) then it will jump to Belarus

Note: you won't "see" what you type because there is no input field. It's just a quick way of typing a few letters and jumping to the nearest match which could help you to navigate quicker in a list. If you do want more control over the searching algorithm and if you want to see what you type, then you can look at the Combobox component instead.

That said, there is a bit of a problem. If you include an emoji in the beginning (for example a country flag) then typing bel for Belgium won't work because you need to type the emoji first.

To solve this, we will strip out emojis from the text value representation of the option.

<Listbox>
  <Listbox.Button>Country</Listbox.Button>
  <Listbox.Options>
    <Listbox.Option value="us">🇺🇸 United States of America</Listbox.Option>
    <Listbox.Option value="ca">🇨🇦 Canada</Listbox.Option>
    <Listbox.Option value="be">🇧🇪 Belgium</Listbox.Option>
    <Listbox.Option value="by">🇧🇾 Belarus</Listbox.Option>
  </Listbox.Options>
</Listbox>

This example will behave the same as the example without the flags.

If you open the Listbox, then type:

  • united then it will jump to the United States of America
  • bel then it will jump to the Belgium
  • bel (again) then it will jump to Belarus

But there is an additional problem. It could be that the contents of the option contains "image"-like text, such as ASCII art. Or for a more realistic example, an icon font that uses some special characters to display a certain "image" using a font.

Example using ASCII art:

<Listbox>
  <Listbox.Button>Country</Listbox.Button>
  <Listbox.Options>
    <Listbox.Option value="us">🇺🇸 United States of America</Listbox.Option>
    <Listbox.Option value="ca">🇨🇦 Canada</Listbox.Option>
    <Listbox.Option value="be">🇧🇪 Belgium</Listbox.Option>
    <Listbox.Option value="by">🇧🇾 Belarus</Listbox.Option>
    <Listbox.Option value="delete">
      <div role="img">
        <p>(╯°□°)╯︵ ┻━┻</p>
      </div>
      Delete
    </Listbox.Option>
  </Listbox.Options>
</Listbox>

This now means that you won't be able to "delete", because these strange characters are in front of the "delete" word. They are also not emojis so they won't be stripped by default.

To solve this, we will also make sure to drop any text content that is in an element with one of these characteristics:

  • Contains a hidden attribute (e.g.: <div hidden>Hidden for everyone</div>)
  • Contains a aria-hidden attribute (e.g.: <div aria-hidden="true">Hidden for assistive technology</div>)
  • Contains a role="img" attribute (e.g.: <div role="img">image-like text</div>)

This has to crawl the DOM and can be expensive, however in some scenarios you know exactly what should happen, in those cases you can also drop all the "visual" artifacts and use an aria-label="delete" attribute to describe what this means exactly.

Note: Assistive technology will also use aria-label and aria-labelledby when it is present on the element instead of the contents of the element.

Now you have complete control over the items:

<Listbox>
  <Listbox.Button>Country</Listbox.Button>
  <Listbox.Options>
    <Listbox.Option value="us" aria-label="united states of america">
      🇺🇸 United States of America
    </Listbox.Option>
    <Listbox.Option value="ca" aria-label="canada">
      🇨🇦 Canada
    </Listbox.Option>
    <Listbox.Option value="be" aria-label="belgium">
      🇧🇪 Belgium
    </Listbox.Option>
    <Listbox.Option value="by" aria-label="belarus">
      🇧🇾 Belarus
    </Listbox.Option>
    <Listbox.Option value="del" aria-label="delete">
      <div role="img">
        <p>(╯°□°)╯︵ ┻━┻</p>
      </div>
      Delete
    </Listbox.Option>
  </Listbox.Options>
</Listbox>

To be complete with the aria-label attribute, this PR also tries to resolve the contents of the element referenced via aria-labelledby if you use that attribute instead.

Fixes: #2454

@vercel
Copy link

vercel bot commented May 2, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
headlessui-react ✅ Ready (Inspect) Visit Preview 💬 Add feedback May 4, 2023 0:39am
headlessui-vue ✅ Ready (Inspect) Visit Preview 💬 Add feedback May 4, 2023 0:39am

Copy link
Contributor

@thecrypticace thecrypticace left a comment

Choose a reason for hiding this comment

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

Looks good. Just one small change re: the regex and a question about eliminating the hidden elements.

This makes it a bit slower but also more correct. We can use a cache on
another level to ensure that we are not creating useless work.
This will add a cache and only if the `innerText` changes, only then
will we calculate the new text value.
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.

Can't search non-alphanumeric Options in Listbox
2 participants