Skip to content

Attachment table: keep the search suggestions aligned with the field - #1209

Merged
bert-e merged 7 commits into
development/1.0from
bugfix/CUI-attachment-search-dropdown-width
Oct 19, 2026
Merged

bert-e merged 7 commits into
development/1.0from
bugfix/CUI-attachment-search-dropdown-width

Conversation

@JeanMarcMilletScality

@JeanMarcMilletScality JeanMarcMilletScality commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR — Attachment table: the entity search's suggestion list no longer hangs past the field when the panel narrows, a long name no longer runs out of the list, and tabbing out of an open list no longer attaches whichever suggestion was under the cursor.

before after
the list overhangs the field the list matches the field

NarrowPanel attachment story, list open, the panel then narrowed directly in the DOM so that no re-render refreshes the measurement — the field goes from 301px to 271px. The dashed red rule is the field's right edge, drawn for the capture at the same place in both shots. The list overhangs it by 30px on the left and sits against it on the right.

Context / Why

The list is anchored to the search box, and until #1201 that box could not change width, so a width measured once was always right. Making the box shrinkable is what exposed the measurement: the field now narrows with its container, and the list did not follow.

🧩 Approach

The width came from a DOM measurement held in React state. It is now two CSS insets on a wrapper that is itself exactly as wide as the field, so there is no width to keep in sync at all.

Before

const [searchWidth, setSearchWidth] = useState('0px');

<SearchBoxContainer
  ref={(element) => {                                                 // ←
    if (element?.firstElementChild) {
      setSearchWidth(
        element.firstElementChild.getBoundingClientRect().width - 2 + 'px',
      );
    }
  }}
>
  <MenuContainer {...getMenuProps()} $width={searchWidth} />          // ←

After

const SearchAnchor = styled.div`
  position: relative;
  width: fit-content;                      /* ← the field's width, not the padding box's */
  max-width: 100%;
  min-width: ${SEARCH_INPUT_MIN_WIDTH};    /* ← and its floor, read from SearchInput */
`;

<SearchBoxContainer>
  <SearchAnchor>
    <StyledSearchInput … />
    <MenuContainer {...getMenuProps()} />   /* left: 0; right: 0 */
  </SearchAnchor>

fit-content alone tracks the field down to the point where the field stops: past its own
min-width the field holds its width and overflows, while the anchor kept shrinking with the
panel, which left the list up to 48px narrower than the field under a 140px frame. SearchInput
now exports that floor as SEARCH_INPUT_MIN_WIDTH and the anchor reads it, so the two overflow
together.

Two behaviours move with it.

The field's seam with the open list was drawn from :focus-within while the list used its own open state — two different events, since an outside click blurs the input on mousedown and closes the list on the mouseup that follows. Both now read the same open state, so the seam and the list go together.

Blur also reaches downshift again, which closes the list — wanted — but commits the highlighted row on the way, and committing one runs onSelectedItemChange, which attaches the entity. A state reducer keeps the close and drops the selection:

stateReducer: (state, { type, changes }) =>
  type === useCombobox.stateChangeTypes.InputBlur
    ? { ...changes, selectedItem: state.selectedItem, inputValue: state.inputValue }  // ←
    : changes,

A third thing becomes visible once the list is held to the field's floor. A suggestion's name was a plain <Text> and the list is overflow: visible, so a name the browser cannot break simply leaves it — jean.dupont@example.com offers no break opportunity, neither at the dot nor at the at-sign. Swept over the NarrowPanel story, it crosses the list's right edge under a 200px panel and ends 33px outside at the floor, 3px past the panel's own border. A name with hyphens does break, and its row grows from 36px to 57px instead.

The typography guideline allows either: wrap, but only at a delimiter, a dot or a dash; or ellipsis, with the full value in a tooltip. The list did neither, while the attached-entities table in this same file already draws the same names through ConstrainedText. Now the list does too:

// after
<ConstrainedText text={item.name} tooltipPlacement="right" />   // ←

lineClamp stays at its default of 1 rather than the table's 2. A list walked with the arrow keys wants a constant row height, and a two-line clamp brings back overflow-wrap: break-word — the mid-segment break the guideline rules out. The placement is not the default either: Tooltip opens upwards, which over the first suggestion means over the search field and the query just typed, so this follows Selectv2, which already places an option's tooltip to the right for the same reason.

The name now stops 8px short of the list's edge at every width, which is the row's own padding, and the full value is in the tooltip wherever it is cut.

🔍 Review focus

  • 🔴 organisms/attachments/AttachmentTable.tsx › useCombobox — the state reducer is what stops blur attaching an entity; without it, Tab out of an open list with a row highlighted or hovered attaches that row. AttachmentTable.test.tsx pins both sides of it: take the reducer out and the blur test fails while the click test still passes.
  • 🟡 same file › StyledSearchInput — the && is load-bearing. Input paints its own border from :hover and :focus-within, and a plain & > div cannot outrank a pseudo-class, so the class is doubled. Dropping an & leaves the open state silently unstyled.
  • 🟡 same file › SearchAnchor, error path — there the anchor wraps the Stack that sits the field beside its Loader, so it hugs field plus gap plus loader and the list draws wider than the field. Only reachable when the search fails while the list is already open: the input is disabled on that branch, so it cannot be focused into the error state. Left as is — closing it means restructuring both branches of the field.
  • 🟡 same file › the suggestion list — ConstrainedText decides whether its tooltip is needed by measuring once at mount, with no ResizeObserver. Narrowing the panel while the list is already open therefore cuts a name without offering its full value until the list is reopened. The behaviour is the component's own, and the attached-entities table below carries it too.
  • ⚪ same file › SearchAnchor — position: relative moved off SearchBoxContainer onto the new wrapper. The list is the only absolutely positioned element in the file, so nothing else changes containing block.
  • ⚪ AttachmentTable.test.tsx — a beforeAll makes jsdom report every element as overflowing. jsdom measures everything at zero, so ConstrainedText would always render the branch a browser takes only when the name fits, and the suite would be exercising markup no user gets. With the stub, all three tests run against the truncated shape.
  • ⚪ jest.config.js — stylesheets now map to the repo's own __mocks__/styleMock.js. The config named identity-obj-proxy, which is in neither package.json nor the lockfile, so any suite importing src/lib/index.ts failed to resolve before it could render anything — which is why this table had no test file to add to. One line, and not a dependency change.
  • ⚪ components/searchinput/SearchInput.component.tsx › SEARCH_INPUT_MIN_WIDTH — the field's floor becomes an export so the anchor reads it instead of repeating it. Not added to the index.ts barrel.

🧪 How to test

  1. Open the NarrowPanel attachment story and click into the entity search so the list opens.
  2. Keeping it open, narrow the panel from devtools rather than with the frameWidth control, so React does not re-render. The list should stay flush with the field.
  3. Click outside. The field's highlight and the list should go at the same moment, not one and then the other.
  4. Keep narrowing, past the point where the field stops shrinking and starts overflowing the panel. The list should overflow with it, staying exactly as wide as the field.
  5. At that width, reopen the list. A name with nothing to break on — an email, an id — should be cut with an ellipsis inside the list rather than running out of it, and hovering it should show the whole name.
  6. Open the list again, hover a suggestion or press ↓, then press Tab. The list should close and nothing should be attached.

🔗 References

What changed

Two comments claimed the search box was a fixed 287px and that this was the table's narrowest point; neither has been true since #1201, and leaving them would send the next reader looking for a floor that no longer exists. One was in this file, the other in the NarrowPanel story.

The list is drawn with the field's own tokens — spacing.r1 and spacing.r4, where its outline
and its row separators carried a literal 1px and 4px. At a 14px root those are 0.875px and 3.5px, so on a 2× display
the list drew two device pixels against the field's one and read as a deeper blue along the same
frame.

The searchInputIsFocused state goes with them, along with the list's focused-but-closed bottom border that was its only consumer — with the seam now keyed on the open state, the field keeps its own border there and already draws that edge.

@bert-e

bert-e commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Hello jeanmarcmilletscality,

My role is to assist you with the merge of this
pull request. Please type @bert-e help to get information
on this process, or consult the user documentation.

Available options
name description privileged authored
/after_pull_request Wait for the given pull request id to be merged before continuing with the current one.
/bypass_author_approval Bypass the pull request author's approval ⭐
/bypass_build_status Bypass the build and test status ⭐
/bypass_commit_size Bypass the check on the size of the changeset TBA ⭐
/bypass_incompatible_branch Bypass the check on the source branch prefix ⭐
/bypass_jira_check Bypass the Jira issue check ⭐
/bypass_peer_approval Bypass the pull request peers' approval ⭐
/bypass_leader_approval Bypass the pull request leaders' approval ⭐
/bypass_source_branch_lineage Bypass the cross-branch contamination check ⭐
/approve Instruct Bert-E that the author has approved the pull request. ✍️
/create_pull_requests Allow the creation of integration pull requests.
/create_integration_branches Allow the creation of integration branches.
/no_octopus Prevent Wall-E from doing any octopus merge and use multiple consecutive merge instead
/unanimity Change review acceptance criteria from one reviewer at least to all reviewers
/wait Instruct Bert-E not to run until further notice.
Available commands
name description privileged
/help Print Bert-E's manual in the pull request.
/status Print Bert-E's current status in the pull request.
/clear Remove all comments from Bert-E from the history TBA
/retry Re-start a fresh build TBA
/build Re-start a fresh build TBA
/force_reset Delete integration branches & pull requests, and restart merge process from the beginning.
/reset Try to remove integration branches unless there are commits on them which do not appear on the source branch.

Status report is not available.

@bert-e

bert-e commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Waiting for approval

The following approvals are needed before I can proceed with the merge:

  • the author

  • one peer

Peer approvals must include at least 1 approval from the following list:

JeanMarcMilletScality and others added 2 commits October 1, 2026 14:59
The suggestion menu took its width from a measurement of the search box,
read in a ref callback and held in state. That refreshed on every render
but never on a resize that causes no render -- a container query, or a
side panel narrowing the content box -- so the menu kept a width the
field no longer had. It could not bite while the box was pinned to one
width; making the box shrinkable is what exposed it.

Anchor the menu to the search container's padding box with left/right
insets instead, so it follows the field with no measurement and no state.
Both insets set means the used width falls out of the over-constrained
equation, which already accounts for the menu's own borders -- so the
`- 2` goes too, and the menu now lines up with the field's outer edge
instead of sitting a pixel short on each side.

Also drops two notes that stopped being true when the box became
shrinkable: it is no longer a fixed 287px, and no longer this table's
narrowest point.
…t with the seam

The previous commit anchored the menu to `SearchBoxContainer`'s padding box,
which is wider than the field, since the field hugs its own content. A
`SearchAnchor` wrapper with `width: fit-content` becomes the menu's containing
block instead, so `left: 0; right: 0` resolves to the field's width. Measured
over one menu held open while the panel resized underneath it with no re-render,
600px down to 220px and back: the menu tracked the field to 0px at every step,
against an overhang reaching 95px before.

The field's seam with the open menu moves off `:focus-within` onto the menu's
own open state. An outside click blurs the input on `mousedown` while the menu
closes on the `mouseup` that follows, so a focus-driven seam snapped back under
a menu that was still open. `&&` is required on the new rule: `Input` paints its
border from `:hover` and `:focus-within`, both of which outrank a single-class
`& > div`.

Dropping the consumer's `onBlur` also restores downshift's own, which it had
been overriding by being spread after `getInputProps()`, so tabbing out of the
field now closes the menu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@JeanMarcMilletScality
JeanMarcMilletScality force-pushed the bugfix/CUI-attachment-search-dropdown-width branch from 363af93 to 8b557f0 Compare October 1, 2026 13:00
@bert-e

bert-e commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Waiting for approval

The following approvals are needed before I can proceed with the merge:

  • the author

  • one peer

Peer approvals must include at least 1 approval from the following list:

Comment thread src/lib/organisms/attachments/AttachmentTable.tsx
Comment thread src/lib/organisms/attachments/AttachmentTable.tsx
…ses focus

Handing blur back to downshift also handed it the selection it performs
there: on InputBlur it commits the highlighted row, and committing one runs
onSelectedItemChange, which attaches the entity. Tabbing out of an open list
with a row under the cursor attached it.

A state reducer keeps the close and drops the selection, so blur does the one
thing it is wanted for.

Three comments come down with it. Two restated what the CSS already says, and
the third narrated the event ordering behind the seam; the specificity bump on
the seam rule is the one part a reader cannot derive, and it stays.
@JeanMarcMilletScality
JeanMarcMilletScality marked this pull request as ready for review October 1, 2026 13:25
… floor

The anchor the list hangs off tracked the field with `fit-content`, which
follows it only while the field is still shrinking. Past its own `min-width`
the field holds its width and overflows its container, while the anchor kept
shrinking with the panel, so the list came out up to 48px narrower than the
field it belongs to. `SearchInput` exports that floor now, and the anchor
reads it rather than spelling it out a second time.

The list's outline also carried a literal 1px border and 4px radius against
the field's `spacing.r1` and `spacing.r4`. At a 14px root those are 0.875px
and 3.5px, so on a 2x display the list drew two device pixels against the
field's one, along the same frame.
@bert-e

bert-e commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Waiting for approval

The following approvals are needed before I can proceed with the merge:

  • the author

  • one peer

Peer approvals must include at least 1 approval from the following list:

@JeanMarcMilletScality
JeanMarcMilletScality marked this pull request as draft October 7, 2026 17:13
…ching on blur

The css mapper named identity-obj-proxy, which is in neither package.json nor the
lockfile, so nothing that imports src/lib/index.ts could be tested -- and this table
does. It now points at the stub already sitting in __mocks__.
…r token

spacing.f1 is a fixed pixel used nowhere else on a border; spacing.r1 is the token
every other border in the library uses, including the field's own, so the list and
the field now hold the same hairline at any root size.
@bert-e

bert-e commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Waiting for approval

The following approvals are needed before I can proceed with the merge:

  • the author

  • one peer

Peer approvals must include at least 1 approval from the following list:

@JeanMarcMilletScality
JeanMarcMilletScality marked this pull request as ready for review October 16, 2026 16:59
@eve-scality

eve-scality commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

🟡 Approve with nits

Good to merge as it stands — the point(s) below are nits: worth closing, not required.

Findings

  • ⚠️ Tighten the click-to-attach assertion to catch spurious extra attachments

  • ⚠️ Every test fixes useCombobox.type at 'USER'; the other state is never reached

This review is advisory and does not block the merge.

Full review

Findings

  • ⚠️ Tighten the click-to-attach assertion to catch spurious extra attachments
    src/lib/organisms/attachments/AttachmentTable.test.tsx:73
    Line 73 uses toContain('User B') to verify that clicking User B produced an attachment. The helper attachedNames() accumulates every ADD operation emitted across all calls to onAttachmentsOperationsChanged. If a bug caused clicking User B to also attach User A — or to fire two ADD operations for User B — toContain would still pass, because it only tests membership, not the full list. Since the fixture starts with no initial entities and the test performs a single click, the complete set of attachments is fully determined: exactly one ADD, exactly 'User B'. toEqual(['User B']) expresses that guarantee and would catch the spurious-attachment case.
    Suggestion: Change line 73 to: await waitFor(() => expect(attachedNames()).toEqual(['User B']));
  • ⚠️ Every test fixes useCombobox.type at 'USER'; the other state is never reached
    src/lib/organisms/attachments/AttachmentTable.tsx
    The code behaves differently depending on useCombobox.type (a behavior holds only when it is useCombobox.stateChangeTypes.InputBlur). Every test sets it to 'USER' and none makes the fixture return a different value, so the behavior that needs the other value never happens in the suite. That behavior is not weakly covered, it is unassertable: removing the guard that produces it leaves every test green.
    Suggestion: Add a test that makes the useCombobox fixture return type at a different value, and assert the behavior that depends on it.

A name the browser cannot break -- an email, an id -- left the list
entirely, by 33px once the field sits on its floor, and past the panel's
own border with it. The typography guideline allows wrapping at a
delimiter or an ellipsis carrying the full value in a tooltip; the list
did neither, while the table of attached entities in this same component
already draws the same names through ConstrainedText.

The tooltip goes to the right rather than the default top, which over
the first suggestion would cover the search field and the query just
typed -- the placement Selectv2 already uses for an option's tooltip.
@bert-e

bert-e commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

Waiting for approval

The following approvals are needed before I can proceed with the merge:

  • the author

  • one peer

Peer approvals must include at least 1 approval from the following list:

@bert-e

bert-e commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

Waiting for approval

The following approvals are needed before I can proceed with the merge:

  • the author

  • one peer

Peer approvals must include at least 1 approval from the following list:

@JeanMarcMilletScality

Copy link
Copy Markdown
Contributor Author

/approve

@bert-e

bert-e commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

In the queue

The changeset has received all authorizations and has been added to the
relevant queue(s). The queue(s) will be merged in the target development
branch(es) as soon as builds have passed.

The changeset will be merged in:

  • ✔️ development/1.0

There is no action required on your side. You will be notified here once
the changeset has been merged. In the unlikely event that the changeset
fails permanently on the queue, a member of the admin team will
contact you to help resolve the matter.

IMPORTANT

Please do not attempt to modify this pull request.

  • Any commit you add on the source branch will trigger a new cycle after the
    current queue is merged.
  • Any commit you add on one of the integration branches will be lost.

If you need this pull request to be removed from the queue, please contact a
member of the admin team now.

The following options are set: approve

@bert-e

bert-e commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

I have successfully merged the changeset of this pull request
into targetted development branches:

  • ✔️ development/1.0

Please check the status of the associated issue None.

Goodbye jeanmarcmilletscality.

@bert-e
bert-e merged commit 04c76c9 into development/1.0 Oct 19, 2026
9 checks passed
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.

4 participants