feat: migrate options, selects, menus and member lists to `Item`

Merged
JU
2d ago
+4281-1442

Proposed changes (including videos or screenshots)

ARCH-2496
Migrates Option, Options, Select and Menu to the Item components, and applies the result across Rocket.Chat in the same PR, so every list built on them changes together.

Fuselage

Renders Option, Options, Select and Menu with the Item family from RocketChat/fuselage#2281. Rows share one layout and one set of states, and the public rcx-option classes stay in place.

Item

  • Menu-item rows (role="menuitem*") are named by their ItemTitle and described by a labelled ItemIcon, as an ItemLink is.
  • New focusVisible prop shows the focus ring for focus moved by a script, such as a menu's. The browser's :focus-visible also matches on hover there.

Menu

  • MenuItem, submenu triggers and MenuSection render as Item, ItemGroupHeader and ItemGroup.

Option, Options and Select

  • Option renders as an Item row. The Option* parts, and their MenuItem* aliases, render as the matching Item part: ItemMedia, ItemIcon, ItemContent, ItemTitle, ItemDescription and ItemActions.
  • OptionHeader and OptionTitle render as ItemGroupHeader. Options dividers render as ItemDivider, and Select sections as ItemGroupHeader and ItemGroup.
  • New iconLabel prop on Option names what its icon conveys. Without it, the icon stays decorative.

Fixes

  • Options scrolls to the keyboard cursor when renderItem renders an Item.
  • The CheckOption checkbox is hidden from assistive technology and made inert, so selection is announced once, through aria-selected.
  • OptionSkeleton placeholder rows are hidden from assistive technology.
  • The Options empty state is a disabled option, so the listbox only contains options.

Rocket.Chat

  • Autocompletes (rooms, users, teams, departments, agents, contacts, peers) and the UiKit user and channel selects use Item parts. Avatars use ITEM_MEDIA_SIZE, and status or room-type icons are named with iconLabel.
  • Room members, banned users and team channels render as Item rows. Member groups use an ItemGroupHeader instead of MembersListDivider, and row menus mount through useDeferredMenuMount.
  • Call members, participants and the chat access modal render as Item rows. The members panel groups "In call" and "Not in the call" as named lists.
  • The category menu's "Create new" item uses GenericMenuItem.

Issue(s)

Replaces RocketChat/fuselage#2310, and takes over the options, selects, menus and member-list parts of #42520.

Steps to test or reproduce

  • yarn workspace @rocket.chat/fuselage storybook: the Menu, Option, Options and Select stories.
  • In the app: room/user/team autocompletes, a UiKit modal with user and channel selects, the room members, banned users and team channels contextual bars, the user menu, and a video conference's members panel. Check with a keyboard and a screen reader.

Further comments

Part of the Item migration, split by area (fuselage change + its application together, so each area looks finished when it merges):

  • this PR: options, selects, menus and member lists (merge first)
  • #42819: sidebar and side panel
  • #42820: composer popups and ItemMeta truncate (stacked on this PR)
  • #42832: lists that only use Item (independent)

Bottom commits from other PRs, dropped on rebase once they land: #42829 (hidden-actions gap fix), #42831 (useDeferredMenuMount in ui-client), #42830 (ITEM_MEDIA_SIZE).

Commit order: fuselage commits first (replayed from the Fuselage PR), then test: adapt app tests… with the adaptations the fuselage change forces on its own (the CheckOption checkbox is hidden from assistive technology; MenuItemContent wraps its text in an ItemTitle), then the app commits.

Why Options and Menu are in one PR: the public MenuItem* parts re-export the Option* parts, so migrating one without the other breaks every menu.

Jest snapshots and the visual-regression baselines (arm64 image) were regenerated here. fuselage.api.md isn't part of this repository, so those commits were dropped.

🤖 Generated with Claude Code