Skip to content

16964 frontend [ ui] Standardize dropdown options - #321

Open
aicarma-artyom-maslov wants to merge 10 commits into
masterfrom
frontend/ui/16964__dropdown_standardization
Open

16964 frontend [ ui] Standardize dropdown options#321
aicarma-artyom-maslov wants to merge 10 commits into
masterfrom
frontend/ui/16964__dropdown_standardization

Conversation

@aicarma-artyom-maslov

@aicarma-artyom-maslov aicarma-artyom-maslov commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

1. Release notes

Standardized dropdown usage by separating value selection from action menus. DropdownList and UsersDropdown use the existing react-select selection foundation, while Dropdown and ModifyDropdown remain Popper-based action menus. Fixed the display regressions this split introduced: filter/sorting menus are aligned with their pill again, submenus drop under their row as on production, and the selection menu no longer slides under page content or collapses onto a narrow toggle.

2. Context

  • Issue: #16964
  • App Location: Shared UI dropdowns, including form selects, user/group pickers, task performers, template conditions, list filters, sorting controls, and edit/delete action menus.
  • Related Changes: Keeps DropdownSurface, DropdownControl, and DropdownOption for shared visuals; restores react-select as the selection engine for DropdownList; keeps Dropdown as the Popper-based action-menu engine.

3. Solution & Implementation Details

Selection engine (DropdownList / UsersDropdown)

  • Reworked DropdownList back onto react-select for value selection, preserving the branch’s shared control, menu surface, option row, static menu, search, grouped options, controlled/uncontrolled values, Formik integration, and action metadata behavior.
  • Kept UsersDropdown as a domain preset over DropdownList, preserving avatars, checkboxes, Invite, All Users, select/deselect callbacks, and invited-user updates.
  • Built the custom components object once per instance and fed it the current render’s data through a ref. A fresh object per render changed the component identities, which made react-select remount the whole menu mid-interaction and detach the option node between pointerdown and click — that broke role selection in CreateUserModal.
  • Restored outside-click dismissal with OutsideClickHandler: react-select only closes its menu when its own Input blurs, and the custom Control renders a toggle button instead of that input.
  • Set backspaceRemovesValue={false}. The in-menu search field is a plain input, so Backspace on an empty query used to pop the last selected value and emit an undeclared pop-value action.
  • Exposed the menu as role="listbox" with aria-multiselectable, the toggle as aria-haspopup="listbox", and gave the keyboard-focused option a visible highlight via isFocused (the previous role="menu" + role="option" pairing was invalid).
  • Moved menu stacking, positioning and shrink-to-fit width into the emotion styles factory, which is the only layer that can reach the react-select wrapper: z-index: 1000 to match the base Dropdown (the default 1 slid the menu under page content), position: relative for static menus, and a transparent background for the compact sm control, which paints its own chip.
  • Gave the menu surface a real width (30.4rem for sm, 100% for lg) with overflow: hidden. The previous --dropdown-menu-width custom property is only read by Dropdown.css, so it went dead once DropdownList stopped composing Dropdown, and the compact menu collapsed onto the toggle width.

Filters and sorting (SelectMenu / FilterSelect)

  • Dropped direction="right", which resolved to a bottom-end Popper placement and pushed the menu leftwards out of alignment. On production the equivalent bootstrap class sat on the container rather than the menu, so those menus have always been left-aligned.
  • Kept the pill padding on .container and aligned the menu with compensating margins (margin-top: 1.2rem, margin-left: -1.6rem) instead of moving the padding onto the toggle. .dropdown-toggle carries all: unset, which wipes padding declared on the toggle depending on stylesheet order — that flattened the workflow toolbar pills from 32px to 20px.
  • Passed the missing searchPlaceholder to the task template and step filters, which rendered an empty search strip.

Action menus (Dropdown / ModifyDropdown)

  • Kept Dropdown / ModifyDropdown as action menus for edit/delete/clone/profile-style commands, confirmation items, submenus, outside-click dismissal, Escape dismissal, and Popper placement.
  • Submenus now open under their row (placement="bottom-start") and line up with the parent menu, matching production; the previous right-start fly-out landed outside it.
  • Added box-sizing: border-box to .dropdown-toggle. all: unset resets it to content-box, so a submenu row styled as a full-width dropdown item overflowed its menu by the item padding — visible as the highlight sticking out past the Snooze row.

Storybook

  • Consolidated eleven story files into two, mirroring the split: UI/Dropdowns/Dropdown (action menus, presets, building blocks) and UI/Dropdowns/Select (value selection plus the FilterSelect, SelectMenu and UsersDropdown presets).
  • Each story reserves vertical room through a room parameter, so an opened menu is never clipped or scrolled to, and single controls stay narrow via storyWidth.
  • Stories with a menu open it automatically with a play function, and variant galleries use a fixed grid so cases line up.
  • Kept touched component implementation files below the 200-line component limit by keeping the react-select component adapters in DropdownListMenu.tsx.

4. What to Test

4.1 Preconditions

  1. Open a template with task performers, conditions, user/group options, and long option labels.
  2. Open a Task Card where performers and guests can be edited.
  3. Open the Tasks and Workflows lists, which carry the template/step filters and the sorting control.
  4. Ensure a screen with edit/delete action menus and a Snooze submenu is available.

4.2 Positive Scenarios / Testing Report

Scenario Steps Expected Result Chrome Desktop Safari Desktop Chrome Mobile Safari Mobile
Generic value select Open a DropdownList form field and select an option. Value updates once and the selected label appears in the control. [ ] [ ] [ ] [ ]
Multi value select Open a multi DropdownList, select and deselect options. Options toggle selected state; multi menu stays open by default. [ ] [ ] [ ] [ ]
Searchable select Search in DropdownList / UsersDropdown. Matching options remain and empty state appears when there are no matches. [ ] [ ] [ ] [ ]
Outside click Open any selection menu and click elsewhere on the page. Menu closes without changing the value. [ ] [ ] [ ] [ ]
Keyboard navigation Open a searchable select and use Arrow keys, then Enter. The focused option is visibly highlighted and Enter selects it. [ ] [ ] [ ] [ ]
User picker Add/remove users/groups and use Invite / All Users. Avatars, checkboxes, badges, actions, and callbacks remain correct. [ ] [ ] [ ] [ ]
Add performer menu Open "Add performer" on a Task Card. Menu keeps its own width, labels are not truncated, and it renders above the Complete Task panel. [ ] [ ] [ ] [ ]
List filters Open the template and step filters on Tasks. Menu is flush with the left edge of the pill, 6px below it, and the search field shows its placeholder. [ ] [ ] [ ] [ ]
Sorting control Open the sorting control on Tasks and in the Task Card timeline. Menu is left-aligned under the pill; pill height is unchanged. [ ] [ ] [ ] [ ]
Action menu Open Dropdown / ModifyDropdown and click Edit/Delete/Clone. Menu items run actions rather than changing a selected value. [ ] [ ] [ ] [ ]
Submenu Open the Snooze / Return to submenu. Submenu drops under its row, lines up with the parent menu, and the row highlight stays inside the menu. [ ] [ ] [ ] [ ]
Storybook Open the Dropdown and Select story groups. Every variant renders, opened menus are fully visible without scrolling, and docs describe the select-vs-action-menu split. [ ] [ ] [ ] [ ]

4.3 Negative Scenarios & Edge Cases

Scenario Steps Expected Result Chrome Desktop Safari Desktop Chrome Mobile Safari Mobile
Empty search Enter a query with no matches. Only the configured empty state appears. [ ] [ ] [ ] [ ]
Re-select single value Select the already-active single-select option. Value remains selected and emits a select action. [ ] [ ] [ ] [ ]
Backspace in multi select Open a multi select, leave the search empty and press Backspace. Selected values are untouched. [ ] [ ] [ ] [ ]
Disabled select/static menu Try opening or selecting while disabled. Popup does not open and static options remain inert. [ ] [ ] [ ] [ ]
Action option inside select Click an Invite-style option with onClick. Action runs without sending a value change. [ ] [ ] [ ] [ ]
Menu near the viewport edge Open a selection menu low on the page. Menu stays usable and is not hidden behind neighbouring content. [ ] [ ] [ ] [ ]

4.4 Verification Points

  • DropdownList imports and uses react-select for value selection.
  • UsersDropdown remains a DropdownList preset, not an action menu.
  • Dropdown remains Popper-based and is not used as the value-select engine.
  • The selection menu is a listbox with option children; the action menu is a menu with menuitem-family children.
  • No padding or layout property is declared on a Dropdown toggle class, because all: unset can wipe it depending on stylesheet order.
  • Storybook exposes exactly two dropdown story groups, Dropdown and Select, and each covers its variants.
  • Shared visual pieces (DropdownSurface, DropdownControl, DropdownOption) are still used where appropriate.

4.5 API Verification

  • No API contracts or endpoints were changed.
  • Dropdown values continue to use existing form/component state mappings.
  • No API requests or payloads were manually inspected during this verification.

4.6 What Was NOT Tested

  • Manual Chrome/Safari desktop and mobile matrix testing remains unchecked in the tables above.
  • The "Add performer" menu overlapping the Complete Task panel was fixed by matching the base Dropdown stacking order, but could not be reproduced in Storybook and was not re-checked in the running app.
  • The Storybook render sweep (28 stories, console errors and menu clipping) was run before the final tweak that makes the compact sm control background transparent; it was not repeated afterwards. Type-check, lint and the full Jest suite were re-run after that tweak.
  • npm run build-storybook was not re-run after the story consolidation.
  • API traffic and persisted browser state were not manually verified.

5. Affected Areas

Area Change Chrome Desktop Safari Desktop Chrome Mobile Safari Mobile
DropdownList react-select-backed value selection with stable component identities, outside-click dismissal, Backspace guard, listbox semantics, focus highlight, menu stacking and width. [ ] [ ] [ ] [ ]
UsersDropdown Continues as a user/group selection preset over DropdownList. [ ] [ ] [ ] [ ]
SelectMenu / FilterSelect Left-aligned menus flush with the pill; pill padding and height restored; missing search placeholder added to the task filters. [ ] [ ] [ ] [ ]
Dropdown / ModifyDropdown Remain Popper-based action menus; submenus drop under their row and item highlights stay inside the menu. [ ] [ ] [ ] [ ]
Storybook Eleven story files consolidated into Dropdown and Select, with reserved height, auto-opened menus and grid galleries. [ ] [ ] [ ] [ ]
Tests Dropdown/select behavior covered by focused Jest tests, including four new DropdownList regression tests. [ ] [ ] [ ] [ ]

6. Unit Tests

  • Passed: npx jest --ci src/public — 1517 tests in 213 suites, 15 snapshots.
  • Passed: npx tsc --noEmit --skipLibCheck -p tsconfig.json — no project-source errors.
  • Passed: npx eslint src/public/components/UI src/public/layout --ext .ts,.tsx. Story files are covered by the repo’s ESLint ignore pattern.
  • Passed: npx stylelint "src/public/components/UI/**/*.css".
  • Verified with Playwright against a running Storybook: menu/pill geometry (pill height 32px, menu left edge flush with the pill, 6px gap, submenu aligned with its parent, dropdown item overflow 0px) and a render sweep of all 28 stories with no console errors and no clipped menus.
  • Not passed / existing issue: npx tsc --noEmit without --skipLibCheck fails on pre-existing dependency type conflicts in node_modules (@types/enzyme / React duplicate declarations), before surfacing project-file errors.

7. Commits

  • f798a90816964 fix(ui): standardize dropdown options
  • 608efa6016964 refactor(ui): compose dropdown option variants
  • 8f44396416964 refactor(ui): share compact dropdown control
  • 7b4262c516964 fix(ui): address dropdown accessibility review
  • 7891f63616964 refactor(ui): unify dropdown foundations
  • 4517c27816964 refactor(ui): drop react-select from dropdowns
  • 9d4598d516964 fix(ui): address dropdown review findings
  • 7b350a2d16964 fix(ui): restore multi-select reset and single-select change action
  • 9c01661816964 fix(ui): close nested submenus explicitly on option select
  • a16f7e6616964 fix(ui): fix dropdown menu display and consolidate dropdown stories

Note

[!NOTE]

Replace react-select with custom dropdown components across the UI

  • Rewrites DropdownList to use new internal Dropdown, DropdownControl, DropdownSurface, and DropdownListMenu components instead of react-select, adding support for grouped options, built-in search, controlled/uncontrolled selection, and multi-select.
  • Rewrites Dropdown to use react-popper for positioning and native <button> elements with ARIA attributes, closing on outside click and Escape key; submenus now nest Dropdown components.
  • Adds DropdownControl, DropdownSurface, and DropdownListMenu as reusable shared primitives consumed by DropdownList, DropdownArea, DropdownButton, FilterSelect, and SelectMenu.
  • Refactors UsersDropdown, FilterSelect, SelectMenu, DropdownButton, and DropdownArea to consume the new APIs, replacing reactstrap and react-select dependencies.
  • CSS overrides targeting react-select class names are replaced with CSS custom properties (--dropdown-list-control-min-height, --dropdown-menu-width, etc.) across multiple feature stylesheets.
  • Risk: DOM structure, CSS class names, and change-event shapes differ from the previous react-select implementation; DropdownList.onChange now emits {action, option} instead of react-select's ActionMeta, and multi-select no longer renders chips in the control.

Changes since #321 opened

  • Modified DropdownList.handleSelect handler to prevent deselection in single-select mode [7b350a2]
  • Updated FilterSelectMenu component to render reset entry in multi-select mode [7b350a2]
  • Added test coverage for nested submenu collapse behavior in Dropdown component [7b350a2]
  • Modified DropdownOptions component to use render-prop pattern for submenu rendering that invokes both closeSubmenu() and closeDropdown() callbacks when a nested option is selected [9c01661]
  • Added test case to Dropdown test suite verifying that selecting a deeply nested option closes all menu levels [9c01661]
  • Refactored DropdownList component to use react-select library instead of custom dropdown implementation [a16f7e6]
  • Changed dropdown submenu positioning to open below parent rows within the parent menu instead of flying out to the right [a16f7e6]
  • Consolidated Storybook stories by deleting individual component stories and creating comprehensive stories in Dropdown.stories.tsx and Select.stories.tsx [a16f7e6]
  • Removed forced right alignment from FilterSelect and SelectMenu components and adjusted dropdown menu margins in Select.css [a16f7e6]
  • Added search placeholder prop to FilterSelect components in TasksLayout.TasksLayoutComponent [a16f7e6]

Macroscope summarized 9d4598d.

Comment thread frontend/src/public/components/UI/DropdownList/DropdownList.tsx Outdated
Comment thread frontend/src/public/components/UI/DropdownList/DropdownList.css Outdated
Comment thread frontend/src/public/components/UI/DropdownList/DropdownList.css Outdated
Comment thread frontend/src/public/components/UI/Dropdown/DropdownOptions.tsx
Comment thread frontend/src/public/components/UI/Buttons/DropdownButton/DropdownButton.tsx Outdated
Comment thread frontend/src/public/components/UI/Select/FilterSelect.tsx Outdated
));
const menuContent = renderMenuContent?.(renderedContent) || renderedContent;
const isWide = Array.isArray(options) && options.length > 0 && options.every((option) => option.size === 'lg');
const menu = menuContent ? (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High Dropdown/Dropdown.tsx:91

Submenus are clipped and inaccessible: DropdownOptions renders nested Dropdown components inside the parent menu wrapper, and Popper positions them at placement="right-start" so they extend beyond the parent menu's bounds. Because the parent menu wrapper (styles['dropdown-menu']) applies overflow: hidden, the submenu fly-out is cut off and subOptions/customSubOption content can't be seen or interacted with.

The previous implementation used UncontrolledDropdown/DropdownMenu from reactstrap, which portaled each submenu independently. Consider either removing the overflow: hidden constraint on .dropdown-menu or portaling submenu Dropdown elements out of the parent menu so Popper can position them without clipping.

Also found in 1 other location(s)

frontend/src/public/components/UI/Dropdown/Dropdown.css:34

.dropdown-menu now sets overflow: hidden, but nested submenu Dropdowns are rendered inside the parent menu and Popper places them at right-start. The submenu therefore extends outside the parent menu's bounds and is clipped, making subOptions/customSubOption menus inaccessible.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @frontend/src/public/components/UI/Dropdown/Dropdown.tsx around line 91:

Submenus are clipped and inaccessible: `DropdownOptions` renders nested `Dropdown` components inside the parent menu wrapper, and Popper positions them at `placement="right-start"` so they extend beyond the parent menu's bounds. Because the parent menu wrapper (`styles['dropdown-menu']`) applies `overflow: hidden`, the submenu fly-out is cut off and `subOptions`/`customSubOption` content can't be seen or interacted with.

The previous implementation used `UncontrolledDropdown`/`DropdownMenu` from `reactstrap`, which portaled each submenu independently. Consider either removing the `overflow: hidden` constraint on `.dropdown-menu` or portaling submenu `Dropdown` elements out of the parent menu so Popper can position them without clipping.

Also found in 1 other location(s):
- frontend/src/public/components/UI/Dropdown/Dropdown.css:34 -- `.dropdown-menu` now sets `overflow: hidden`, but nested submenu `Dropdown`s are rendered inside the parent menu and Popper places them at `right-start`. The submenu therefore extends outside the parent menu's bounds and is clipped, making `subOptions`/`customSubOption` menus inaccessible.

dropdownRef={ref}
placement={placement}
className={containerClassName}
menuClassName={styles['content']}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High DropdownArea/DropdownArea.tsx:22

The Add Guest form in GuestsController is clipped: passing styles['content'] as menuClassName merges it with the .dropdown-menu class, which sets a fixed 19.2rem width and overflow: hidden on the popover. That container is only sized for narrow option menus, not the wider 272px guest form (plus 1.6rem padding), so content overflows and is cut off instead of being sized to its contents. Consider using a different prop (or overriding the width/overflow) so DropdownArea content retains its natural size.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @frontend/src/public/components/UI/DropdownArea/DropdownArea.tsx around line 22:

The Add Guest form in `GuestsController` is clipped: passing `styles['content']` as `menuClassName` merges it with the `.dropdown-menu` class, which sets a fixed `19.2rem` width and `overflow: hidden` on the popover. That container is only sized for narrow option menus, not the wider `272px` guest form (plus `1.6rem` padding), so content overflows and is cut off instead of being sized to its contents. Consider using a different prop (or overriding the width/overflow) so `DropdownArea` content retains its natural size.

Comment thread frontend/src/public/components/UI/Select/FilterSelectMenu.tsx Outdated
Comment thread frontend/src/public/components/UI/DropdownList/DropdownList.tsx Outdated
Comment thread frontend/src/public/components/UI/DropdownList/DropdownList.css Outdated
@aicarma-artyom-maslov
aicarma-artyom-maslov force-pushed the frontend/ui/16964__dropdown_standardization branch from 07f20fe to 9d4598d Compare August 7, 2026 20:01
Comment thread frontend/src/public/components/UI/DropdownList/DropdownList.tsx Outdated
Comment thread frontend/src/public/components/UI/Select/FilterSelectMenu.tsx Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Frontend Web client changes request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants