Skip to content

Introduce MultiSelectField (#574) - #718

Open
bedrich-schindler wants to merge 4 commits into
masterfrom
task/574
Open

bedrich-schindler wants to merge 4 commits into
masterfrom
task/574

Conversation

@bedrich-schindler

Copy link
Copy Markdown
Contributor

Closes #574

@github-actions

github-actions Bot commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://react-ui-org.github.io/react-ui/pr-preview/pr-718/

Built to branch gh-pages at 2026-10-19 11:15 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@bedrich-schindler

Copy link
Copy Markdown
Contributor Author

⚠️ @adamkudrna I force pushed into this branch as last fixup migrated code from JS to TS.

@adamkudrna adamkudrna left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please refactor the <div contenteditable> to a native <input role="combobox"> per the ARIA Combobox pattern. Following it should solve all accessibility issues of the current solution, mainly the keyboard interaction.

There is the refactoring/multi-select-field-combobox-a11y branch, feel free to use it as inspiration.

Comment thread webpack.config.js
@@ -5,7 +5,7 @@ const TerserPlugin = require('terser-webpack-plugin');
const VisualizerPlugin = require('webpack-visualizer-plugin2');

const MAX_DEVELOPMENT_OUTPUT_SIZE = 3400000;
const MAX_PRODUCTION_OUTPUT_SIZE = 460000;
const MAX_PRODUCTION_OUTPUT_SIZE = 520000;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That's 13 % increase… 😐

import { useEffect } from 'react';
import type { RefObject } from 'react';

export const useClickOutside = (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we reuse the hook in Popover and Modal? It would be a new issue of course.

const callback = jest.fn();
const { getByTestId } = render(<ComponentWithClickOutside callback={callback} />);

fireEvent.click(getByTestId('outside'));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A more typical use case is clicking anywhere on the page. Clicking another button is rather an edge case in the real world usage. I'm wondering if it makes a difference and deserves an own test case.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is it worth an own file? We don't use this pattern in the project.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

👀

</div>
{isDropdownOpen && (
<MultiSelectFieldDropdown
autoFocusFirstOptionOnOpen={!isSearchEnabled}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

With search disabled there's a second focus model: focus jumps straight to the first option on open, while with search enabled it stays in the search span. Two models to keep in sync. With aria-activedescendant, both cases collapse into one: a read-only <input> when search is off.

className={styles.root}
// `plaintext-only` makes the browser insert pasted or dropped content as plain text
// to prevent inserting formatted HTML content into the editable element
contentEditable={disabled ? false : 'plaintext-only'}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

contentEditable="plaintext-only" isn't supported before Firefox 136. There the invalid value falls back to non-editable and search silently stops working. The value also isn't React-controlled (closeDropdown() writes textContent directly), there's no aria-autocomplete or placeholder, and textContent gets fragile once the browser inserts a <br>.

A controlled <input type="text"> avoids all of that, and can be the combobox itself.

}
}}
ref={ref}
role="button"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

As a div role="button" handling only Delete / Backspace, Space scrolls the page and Enter does nothing. The × is aria-hidden and title isn't reliably announced, so a screen reader says "Apple, button" with no hint that it removes.

A real <button aria-label="Remove Apple"> inside the tag gets Enter / Space for free. Spirit also adds a shared hidden description explaining the Delete / Backspace shortcut.

if (keyDetected(
event,
[
...keyBindings.focusNextDropdownItem,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Arrow Down / Up only open the dropdown here, and a second press is needed to reach an option. The APG (and Spirit) open it and activate the first / last option in one go. Home / End aren't supported at all.

- In case of a large amount of options, consider **grouping related items
together** by nesting them.

### Keyboard Interaction

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Worth adding an Accessibility section documenting the ARIA contract (roles and attributes of the input, listbox, options and tags), like the table in Spirit's README. The keyboard list here would change with the points above.

@bedrich-schindler

Copy link
Copy Markdown
Contributor Author

⚠️ Rebased ⚠️

Also mixup with migration to latest TypeScript and Playwright stack was pushed, no further changes applied.

This branch was successfully deployed

1 active deployment
pr-preview/pr-718 — 0e983ca0 Deployed Oct 19, 2026 by bedrich-schindler via Deploy PR Preview #140
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MultiSelect component

2 participants