Skip to content

finding(plugin-detail): RelatedList draws a hand-rolled em-dash instead of the shared EmptyValue, so half its empty cells carry no aria-label #8475

Description

@os-justin

Found while implementing objectui#8459. Filed unassigned, ⛔ not claimed. Out of that card's fence: that card converges the two predicates (when is a cell empty); this is about what the placeholder is once the predicate says yes.

What

@object-ui/components exports EmptyValue, described in its own docblock as the "universal inline placeholder for missing cell/field values — use this anywhere a renderer would otherwise show - or —". It renders:

<span data-slot="empty-value" aria-label="<localized 'No value'>"
      class="select-none text-muted-foreground/50 no-underline pointer-events-none">—</span>

RelatedList.makeCell does not use it. It builds its own:

React.createElement('span', { className: 'text-muted-foreground/50 text-xs italic' }, '—')

Captured from a real rendered grid (origin/main a407bd6):

<div class="truncate w-full"><span class="text-muted-foreground/50 text-xs italic">—</span></div>

No data-slot, no aria-label, no select-none / pointer-events-none, plus text-xs italic that the shared one does not have.

Why it matters

Both placeholders appear in the same grid, on the same screen. makeCell's branch runs first and intercepts the values it recognises; anything it passes through reaches a type-aware renderer that has its own empty branch and returns EmptyValue. Several of those branches are reachable with values makeCell does not classify as empty — e.g. DateCellRenderer returns EmptyValue for an unparseable date (if (date === null || isNaN(date.getTime()))), and ImageCellRenderer for imgs.length === 0.

So two cells in the same column can both read "empty" to a sighted user while only one of them is announced to a screen reader, and they are not visually identical either (italic + text-xs on one).

DetailSection has the same split on the record page: its own em-dash branch is hand-rolled too, but that one at least carries aria-label={t('detail.noValue')} and a title. RelatedList's carries neither — it is the only one of the three with no accessible name at all.

Suggested shape

Have RelatedList.makeCell return <EmptyValue /> (already a dependency — @object-ui/components is a peer). If the text-xs italic treatment is wanted in a dense grid, EmptyValue accepts className, so it can be kept without forking the component or losing the label.

⚠️ Worth checking whether any pin asserts the current class string before changing it, and whether the localized No value label is the right announcement for a grid cell (DetailSection uses detail.noValue).

Evidence level

The RelatedList DOM above is measured (captured from a rendered grid). The claim that a renderer's own EmptyValue branch is reachable from a surviving related-list column is read from source (packages/fields/src/index.tsx), not driven end-to-end — worth one measurement before acting.

Dedup

Searched objectstack-ai/objectui via MCP search_issues (REST is 403 from the dev container): RelatedList EmptyValue placeholder em-dash aria-label inconsistent hand-rolled span. Returned objectui#8459 (control hit — the readings are real), objectui#8279 and objectui#8394; none covers this.

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    bugSomething isn't working
    and removed on Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatfindingpluginpm:queuepriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions