Skip to content

Commit f67bbec

Browse files
feat(plugin-list): a phone list offers Sort in the Display panel and a search that expands in place (#29054)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbFqnLnHCpSEZyrXLojznRbu)_ Fixes #28911 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:手机(390 px)列表现在可以排序和搜索。排序放进已有的「显示」面板(工具栏不加按钮);列表搜索是一个放大镜图标,点开后就地展开成输入框。两者沿用桌面端同一组 `userActions` 开关,桌面端外观不变。 - **为什么改**:此前手机端隐藏了排序和列表搜索,代码注释让用户「换到桌面端」;维护者已裁定两者都在手机端提供(「#28911 和 #28912 都保留,按卡上写的做。」)。 - **风险与回滚**:低。只动 `plugin-list` 的工具栏与「显示」面板;排序面板是把原有排序弹层的内容抽成一份、两处共用,写入路径与桌面端完全相同。「显示」面板在手机端改为屏宽(与筛选、排序弹层一致)。回滚即 revert 本 PR。 - **先看**:① `ui/packages/plugin-list/src/ListView.tsx` 的 `displayOffered` / `sortInDisplay` / `sortPanelBody` 与「Search, phone form」一段;② `ui/packages/plugin-list/src/components/ViewSettingsPopover.tsx` 新增的 `sort` 插槽;③ 新测试 `ListView.phoneSortSearch-28911.test.tsx`。 - **两分钟验证**:`pnpm dev:crm` 后用 390 px 打开 客户 列表:工具栏是 筛选 · 显示 · 搜索;点放大镜输入 glo 只剩 Globex Ltd;打开「显示」顶部是「Sort Records」,加一条排序后卡片顺序随之改变。 - **需要你做的**:无,席位落地。 ## What changes Below the `sm` breakpoint the list toolbar drew Filter and Display only. Sort and the list's own search were `hidden sm:inline-flex`, with comments sending the reader to desktop or calling a list search redundant beside the global search. Both comments are gone. - **Sort rides in the Display panel below `sm`.** The Sort popover's content is now one element, `sortPanelBody`, drawn by the toolbar's Sort popover and by a new `sort` slot of `ViewSettingsPopover`. The slot is `sm:hidden`, so from `sm` up the panel draws nothing new and Sort stays the bar's button. Both write through the same `setCurrentSort` + `emitSortChange` door. It does not count in the Display badge, which counts display settings only. - **Where there is no Display panel**, Sort stays on the bar at every width: `sortInDisplay = toolbarFlags.showSort && displayOffered`, and the bar's Sort trigger hides below `sm` only when `sortInDisplay` holds. `displayOffered` is the Display panel's existing condition, lifted out of the JSX unchanged. So `compactToolbar: false` gets the Sort button on the phone bar, and the toolbar gains no new control in either form. - **Search, phone form.** A `sm:hidden` icon button at the bar's touch size (`h-7 min-w-7`, the same as Filter and Display) expands in place into a field. The field is 16px text, so iOS Safari does not zoom on focus. Enter or Escape folds it back to the icon, which then carries the active keyword as the desktop trigger does. The clear button empties the term and folds it, and blurring an empty field folds it. It writes the same term as the desktop popover (`handleSearchChange`), so the fetch, the export and the desktop trigger all read one value. - **Flags.** Both read the existing `toolbarFlags.showSort` / `toolbarFlags.showSearch`, so `userActions.sort: false` / `userActions.search: false` (and a searchless view or object) withhold them on the phone as on desktop. No new key and no new option. - **Display panel width below `sm`**: `w-[calc(100vw-2rem)] sm:w-80`, as the Filter and Sort popovers already size. At 320 px the Sort rows wrapped "Sort by" onto two lines. - No i18n key is added; the new controls reuse `list.search`, `list.clearSearch`, `table.search` and the Sort panel's existing keys. Card rows and the bottom action area of the #27058 phone layout are unchanged. ## Tests - New `ListView.phoneSortSearch-28911.test.tsx` (9 tests). Narrow and wide are reproduced by mounting the base `.hidden` rule, or that plus the two `sm:` rules, as `ListView.toolbarIconButtons-12695.test.tsx` does. Narrow: no Sort button on the bar, Display carries the Sort panel, and Add sort reaches `onSortChange` and a re-fetch. Wide: Sort on the bar, the Display Sort block not drawn. `userActions.sort: false` withholds both. `compactToolbar: false` keeps Sort on the phone bar at Filter's size. Search: the icon is at Filter's size, tapping opens a focused field in the bar (no popover), typing reaches `onSearchChange` and the fetch, Enter folds to the icon carrying the keyword, Clear search empties and folds, blur on empty folds, wide draws only the desktop icon, `userActions.search: false` withholds it. - `ListView.displayPopover-12423.test.tsx`: the cluster-order pin now lists `phone-search-trigger` after `search-icon-button`. It mounts no stylesheet, so both forms of Search are in the DOM. Ablations (each mutation applied by a script under a `trap` restore, the file restored to `HEAD` byte-identical by hash): - the Sort trigger never hides on the phone (the `sortInDisplay && "hidden sm:inline-flex"` line removed): 1 of 9 red, the narrow Sort test; - the phone search icon drawn at every width (`sm:hidden` removed): 1 of 9 red, the wide search test. ## Acceptance notes - **Screenshots (390 px, `examples/app-crm`, fresh boot, real Console against its backend).** In the project folder `issue-28911/`, for the seat to link: `01-list.png` (bar: Filter · Display · Search), `02-search-expanded.png` (typing `glo` leaves Globex Ltd), `03-search-folded.png` (the icon carries `glo`), `04-display-sort.png` and `05-display-sort-added.png` (Sort Records at the top of Display), `06-sorted-list.png` (the cards in the new order), and `07-desktop-1440-display.png` (1440 px: the bar is unchanged and the Display panel draws no Sort block). - Measured in that run: at 390 px the visible cluster buttons are `toolbar-filter-trigger`, `toolbar-display-trigger` and `phone-search-trigger`, each 28 × 28 px. Searching `glo` leaves one card, and clearing brings back three. Adding a sort in Display re-orders the cards (Initech, Globex Ltd, Acme Corp became Acme Corp, Initech, Globex Ltd). At 1440 px the visible buttons are `toolbar-filter-trigger`, `toolbar-sort-trigger`, `toolbar-display-trigger` and `search-icon-button`, and `display-sort` is not visible. - On the phone the Display trigger's badge does not count an active sort; the desktop Sort trigger's own count badge is the only place a sort count shows. Noted, not changed: the badge counts display settings, and folding sort into it would change its meaning on desktop too. - The SortBuilder's field and direction selects truncate long labels at phone width ("Creat…"), as they already do in the Sort popover at that width. It is the builder's own layout, not changed here. ## Verification At head `8998c718` (this branch with `origin/main` `ab085a04` merged in; the merge brought only additive `@object-ui/i18n` locale keys, no file under `plugin-list` or `components`): - `pnpm exec vitest run packages/plugin-list/` (from `ui/`, `--maxWorkers=2`, under `os-verify-lock`) at `0f0991d3`: 179 / 180 files, 1735 / 1736 tests; the one red was the `displayPopover-12423` cluster-order pin, updated in `51eb0f6f` above. - The 19 `plugin-list` test files that name the Display panel, the Sort or Search triggers (the new file included), at `ee663d49` and again at the merged head `8998c718`: 19 / 19 files, 390 / 390 tests. - 10 consumer test files outside `plugin-list` that name those triggers or the tool cluster (`app-shell` ObjectView / PageView / print-stylesheet / grouped search, `plugin-report` print button): 11 / 11 files with the updated pin, 115 / 115 tests. - `pnpm --dir ui exec vitest run scripts/` at `ee663d49`: 169 files passed, 2 skipped; 4900 tests passed, 2 skipped. - `pnpm --filter @object-ui/plugin-list run type-check`: exit 0 (at `0032651d` and at `8998c718`). `eslint` on the two source files and the new test: 0 errors. - Derived gates (`node scripts/pm/dispatch-gates.mjs --commands`, 34 commands): 34 / 34 exit 0, reconciled by `--ran` (34 derived, 34 run, 0 NOT-MEASURED). Re-run at `8998c718`: `check-sdui-manifest` exit 0, `check-changeset-no-major --base origin/main` exit 0, `check:nul-bytes` exit 0. - `check-changeset-no-major.mjs --base origin/main` with this body as `GITHUB_EVENT_PATH`, and `check-adr-0087-registration`, `check-closing-keyword-parity` and `check-partof-closing-keyword` on this body: exit 0. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (not touched) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: not touched. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01QsR2jSR5zW3jcFhYpwXtjZ --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jSR5zW3jcFhYpwXtjZ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 078e581 commit f67bbec

5 files changed

Lines changed: 436 additions & 77 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@object-ui/plugin-list': patch
3+
---
4+
5+
feat(plugin-list): a phone list offers Sort and its own search
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
Below the `sm` breakpoint the list toolbar used to draw only Filter and Display: Sort and the list-scoped search were hidden, and the only way to find a record by name was the global search. Now:
11+
12+
- **Sort** rides in the Display panel below `sm`, the same Sort panel the toolbar's Sort popover draws, so the bar gains no button. Below `sm` the Display panel now takes the phone's width, as the Filter and Sort panels do. A toolbar with no Display panel (`compactToolbar: false`, or nothing else to display) keeps the Sort button on the bar at every width.
13+
- **Search** is an icon that expands into a field in place. Enter or Escape folds it back to the icon, which then shows the active keyword; the clear button empties the term and folds it.
14+
15+
Both follow the same `userActions.sort` / `userActions.search` flags as the desktop toolbar, and a list whose search resolves to no field still draws none. Nothing a view declares changes.

‎ui/packages/plugin-list/src/ListView.tsx‎

Lines changed: 157 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1892,7 +1892,9 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
18921892
// names it re-runs on a switch into or out of such a view and on nothing else.
18931893
const searchTerm = searchlessViewOnScreen || searchlessObject ? '' : searchInput;
18941894
const [showSearchPopover, setShowSearchPopover] = React.useState(false);
1895-
1895+
// Below `sm` the list search is an icon that expands into a field in place.
1896+
const [phoneSearchOpen, setPhoneSearchOpen] = React.useState(false);
1897+
18961898
// Sort State
18971899
const [showSort, setShowSort] = React.useState(false);
18981900
const [currentSort, setCurrentSort] = React.useState<SortItem[]>(() =>
@@ -5742,6 +5744,85 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
57425744
onRowsChangeRef.current?.(shownRowsRef.current);
57435745
}, [shownRowsKey, shownRowsSettled]);
57445746

5747+
// The Display panel (objectui#12423) is the default form of the toolbar's
5748+
// settings cluster, drawn whenever it has a control to offer.
5749+
const displayOffered = toolbarFlags.compactToolbar && !!(
5750+
viewSwitcherOffered || toolbarFlags.showGroup || toolbarFlags.showColor || toolbarFlags.showDensity
5751+
|| toolbarFlags.showHideFields || currentView === 'grid' || toolbarFlags.showRefresh
5752+
|| (resolvedExportOptions && exportPermitted && exportableFormats.length > 0)
5753+
);
5754+
// Below `sm` the bar keeps Filter and Display only, so Sort rides in the
5755+
// Display panel there. Without a Display panel the Sort trigger stays on the
5756+
// bar at every width. Either way it follows `userActions.sort`.
5757+
const sortInDisplay = toolbarFlags.showSort && displayOffered;
5758+
// The Sort panel: the toolbar Sort popover draws it, and so does the
5759+
// Display panel below `sm` (`sortInDisplay`).
5760+
const sortPanelBody = (
5761+
<div className="space-y-4">
5762+
<div className="flex items-center justify-between border-b pb-2">
5763+
<div className="flex items-center gap-1">
5764+
<h4 className="font-medium text-sm">{t('list.sortRecords')}</h4>
5765+
{/* objectui#12528 — the note on why linked columns are not
5766+
listed, as this control's tooltip and accessible name
5767+
beside the field list, instead of a standing paragraph
5768+
that took about half the popover on every open. Drawn
5769+
under the paragraph's own condition: a relational field
5770+
was left out. */}
5771+
{sortHasRelationalField && (
5772+
<span
5773+
role="img"
5774+
tabIndex={0}
5775+
title={t('list.sortRelationalHint')}
5776+
aria-label={t('list.sortRelationalHint')}
5777+
className="inline-flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
5778+
data-testid="sort-relational-info"
5779+
>
5780+
<Info className="h-3.5 w-3.5" aria-hidden="true" />
5781+
</span>
5782+
)}
5783+
</div>
5784+
{/* Reset to the view's declared sort (objectui#4243).
5785+
Rendered only when the view DECLARES one: with nothing
5786+
declared there is no default to return to, and a control
5787+
under this label that merely cleared the sort would be a
5788+
second, differently-named way to do what removing the
5789+
rows already does. Disabled — not hidden — while the
5790+
active sort already equals the declared one, so the
5791+
affordance stays discoverable and says "you are at the
5792+
default" instead of vanishing. */}
5793+
{declaredSort.length > 0 && (
5794+
<Button
5795+
type="button"
5796+
variant="ghost"
5797+
size="sm"
5798+
className="h-7 px-2 text-xs text-muted-foreground hover:text-primary"
5799+
disabled={!sortDiffersFromDeclared}
5800+
onClick={handleResetSort}
5801+
data-testid="sort-reset-default"
5802+
>
5803+
{t('list.resetSortToDefault')}
5804+
</Button>
5805+
)}
5806+
</div>
5807+
<SortBuilder
5808+
fields={sortFields}
5809+
value={currentSort}
5810+
onChange={(newSort) => {
5811+
// `setCurrentSort` takes the array WHOLE (the in-use
5812+
// exception depends on it); `emitSortChange` is what the
5813+
// host persists. See the boundary's docblock above.
5814+
setCurrentSort(newSort);
5815+
emitSortChange(newSort);
5816+
}}
5817+
/>
5818+
{sortPicksRelationalField && (
5819+
<p className="truncate text-xs text-muted-foreground" data-testid="sort-relational-picked">
5820+
{t('list.sortRelationalPicked')}
5821+
</p>
5822+
)}
5823+
</div>
5824+
);
5825+
57455826
return (
57465827
<div
57475828
ref={pullRef}
@@ -6056,16 +6137,18 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
60566137
</Popover>
60576138
)}
60586139

6059-
{/* Sort — desktop only. Mobile relies on the (typically pre-sorted)
6060-
default view; users who need ad-hoc sorting switch to desktop. */}
6140+
{/* Sort. Below `sm` the Display panel carries the Sort panel when it
6141+
is drawn (`sortInDisplay`), so this trigger hides there; with no
6142+
Display panel it stays on the bar at every width. */}
60616143
{toolbarFlags.showSort && (
60626144
<Popover open={showSort} onOpenChange={setShowSort}>
60636145
<PopoverTrigger asChild>
60646146
<Button
60656147
variant="ghost"
60666148
size="sm"
60676149
className={cn(
6068-
"hidden sm:inline-flex h-7 min-w-7 gap-1 px-1.5 text-muted-foreground hover:text-primary text-xs transition-colors duration-150",
6150+
"h-7 min-w-7 gap-1 px-1.5 text-muted-foreground hover:text-primary text-xs transition-colors duration-150",
6151+
sortInDisplay && "hidden sm:inline-flex",
60696152
currentSort.length > 0 && "text-foreground font-medium"
60706153
)}
60716154
data-testid="toolbar-sort-trigger"
@@ -6083,69 +6166,7 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
60836166
</Button>
60846167
</PopoverTrigger>
60856168
<PopoverContent align="start" className="w-[calc(100vw-2rem)] sm:w-[600px] max-w-[600px] p-3 sm:p-4">
6086-
<div className="space-y-4">
6087-
<div className="flex items-center justify-between border-b pb-2">
6088-
<div className="flex items-center gap-1">
6089-
<h4 className="font-medium text-sm">{t('list.sortRecords')}</h4>
6090-
{/* objectui#12528 — the note on why linked columns are not
6091-
listed, as this control's tooltip and accessible name
6092-
beside the field list, instead of a standing paragraph
6093-
that took about half the popover on every open. Drawn
6094-
under the paragraph's own condition: a relational field
6095-
was left out. */}
6096-
{sortHasRelationalField && (
6097-
<span
6098-
role="img"
6099-
tabIndex={0}
6100-
title={t('list.sortRelationalHint')}
6101-
aria-label={t('list.sortRelationalHint')}
6102-
className="inline-flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
6103-
data-testid="sort-relational-info"
6104-
>
6105-
<Info className="h-3.5 w-3.5" aria-hidden="true" />
6106-
</span>
6107-
)}
6108-
</div>
6109-
{/* Reset to the view's declared sort (objectui#4243).
6110-
Rendered only when the view DECLARES one: with nothing
6111-
declared there is no default to return to, and a control
6112-
under this label that merely cleared the sort would be a
6113-
second, differently-named way to do what removing the
6114-
rows already does. Disabled — not hidden — while the
6115-
active sort already equals the declared one, so the
6116-
affordance stays discoverable and says "you are at the
6117-
default" instead of vanishing. */}
6118-
{declaredSort.length > 0 && (
6119-
<Button
6120-
type="button"
6121-
variant="ghost"
6122-
size="sm"
6123-
className="h-7 px-2 text-xs text-muted-foreground hover:text-primary"
6124-
disabled={!sortDiffersFromDeclared}
6125-
onClick={handleResetSort}
6126-
data-testid="sort-reset-default"
6127-
>
6128-
{t('list.resetSortToDefault')}
6129-
</Button>
6130-
)}
6131-
</div>
6132-
<SortBuilder
6133-
fields={sortFields}
6134-
value={currentSort}
6135-
onChange={(newSort) => {
6136-
// `setCurrentSort` takes the array WHOLE (the in-use
6137-
// exception depends on it); `emitSortChange` is what the
6138-
// host persists. See the boundary's docblock above.
6139-
setCurrentSort(newSort);
6140-
emitSortChange(newSort);
6141-
}}
6142-
/>
6143-
{sortPicksRelationalField && (
6144-
<p className="truncate text-xs text-muted-foreground" data-testid="sort-relational-picked">
6145-
{t('list.sortRelationalPicked')}
6146-
</p>
6147-
)}
6148-
</div>
6169+
{sortPanelBody}
61496170
</PopoverContent>
61506171
</Popover>
61516172
)}
@@ -6279,11 +6300,7 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
62796300
Every control writes through the same door as its wide-form
62806301
button above; the panel adds no storage of its own. Drawn on
62816302
mobile too (icon only): Refresh and Export live in it. */}
6282-
{toolbarFlags.compactToolbar && (
6283-
viewSwitcherOffered || toolbarFlags.showGroup || toolbarFlags.showColor || toolbarFlags.showDensity
6284-
|| toolbarFlags.showHideFields || currentView === 'grid' || toolbarFlags.showRefresh
6285-
|| (resolvedExportOptions && exportPermitted && exportableFormats.length > 0)
6286-
) && (
6303+
{displayOffered && (
62876304
<ViewSettingsPopover
62886305
t={t as any}
62896306
allFields={allFields as any}
@@ -6300,6 +6317,7 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
63006317
/>
63016318
) : undefined}
63026319
recordOpen={recordOpen}
6320+
sort={sortInDisplay ? sortPanelBody : undefined}
63036321
showGroup={toolbarFlags.showGroup}
63046322
groupingConfig={groupingConfig}
63056323
setGroupingConfig={changeGrouping}
@@ -6473,10 +6491,9 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
64736491
) : null;
64746492
})()}
64756493

6476-
{/* Search (icon button + popover) — desktop only. The global
6477-
top-bar search (⌘K) is already prominent on mobile, so an
6478-
additional list-scoped search popover would be redundant chrome.
6479-
Filter remains the primary way to narrow data on phones. */}
6494+
{/* Search, scoped to this list. From `sm` up an icon button opens
6495+
a popover; below `sm` the icon expands into a field in place (the
6496+
phone form after this popover). Both write the same term. */}
64806497
{toolbarFlags.showSearch && (
64816498
<Popover open={showSearchPopover} onOpenChange={setShowSearchPopover}>
64826499
<PopoverTrigger asChild>
@@ -6533,6 +6550,70 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
65336550
</Popover>
65346551
)}
65356552

6553+
{/* Search, phone form: the icon expands into a field in place, with
6554+
the bar's touch size. Enter or Escape folds it back to the icon,
6555+
which then carries the active keyword as the desktop trigger
6556+
does; the clear button empties the term and folds it. */}
6557+
{toolbarFlags.showSearch && (phoneSearchOpen ? (
6558+
<div className="relative flex items-center sm:hidden" data-testid="phone-search">
6559+
<Search
6560+
className="pointer-events-none absolute left-2 h-3.5 w-3.5 text-muted-foreground"
6561+
aria-hidden="true"
6562+
/>
6563+
<Input
6564+
type="search"
6565+
placeholder={t('table.search')}
6566+
aria-label={t('list.search')}
6567+
value={searchTerm}
6568+
onChange={(e) => handleSearchChange(e.target.value)}
6569+
onKeyDown={(e) => {
6570+
if (e.key === 'Enter' || e.key === 'Escape') setPhoneSearchOpen(false);
6571+
}}
6572+
onBlur={() => {
6573+
if (!searchTerm) setPhoneSearchOpen(false);
6574+
}}
6575+
// 16px text: a smaller font makes iOS Safari zoom on focus.
6576+
className="h-7 w-36 pl-7 pr-7 text-base [&::-webkit-search-cancel-button]:hidden"
6577+
autoFocus
6578+
data-testid="phone-search-input"
6579+
/>
6580+
<Button
6581+
variant="ghost"
6582+
size="sm"
6583+
className="absolute right-0 h-7 w-7 p-0 text-muted-foreground hover:text-primary"
6584+
aria-label={t('list.clearSearch')}
6585+
title={t('list.clearSearch')}
6586+
onClick={() => {
6587+
handleSearchChange('');
6588+
setPhoneSearchOpen(false);
6589+
}}
6590+
data-testid="phone-search-clear"
6591+
>
6592+
<X className="h-3.5 w-3.5" />
6593+
</Button>
6594+
</div>
6595+
) : (
6596+
<Button
6597+
variant="ghost"
6598+
size="sm"
6599+
className={cn(
6600+
"sm:hidden h-7 min-w-7 gap-1 px-1.5 text-muted-foreground hover:text-primary text-xs transition-colors duration-150",
6601+
searchTerm && "text-foreground font-medium",
6602+
)}
6603+
aria-label={searchTerm ? `${t('list.search')}: ${searchTerm}` : t('list.search')}
6604+
title={searchTerm ? `${t('list.search')}: ${searchTerm}` : t('list.search')}
6605+
onClick={() => setPhoneSearchOpen(true)}
6606+
data-testid="phone-search-trigger"
6607+
>
6608+
<Search className="h-3.5 w-3.5" />
6609+
{searchTerm && (
6610+
<span className="max-w-[5rem] truncate text-[11px]" data-testid="phone-search-active-keyword">
6611+
{searchTerm}
6612+
</span>
6613+
)}
6614+
</Button>
6615+
))}
6616+
65366617
{/* Add Record (top position) */}
65376618
{toolbarFlags.showAddRecordTop && (
65386619
<Button

‎ui/packages/plugin-list/src/__tests__/ListView.displayPopover-12423.test.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ describe('the list toolbar defaults to Filter · Sort · Display · Search (obje
111111
'toolbar-sort-trigger',
112112
'toolbar-display-trigger',
113113
'search-icon-button',
114+
// Search's phone form (objectstack#28911), drawn below `sm` only. No
115+
// stylesheet is mounted here, so both forms of Search are in the DOM.
116+
'phone-search-trigger',
114117
]);
115118
// Named absences, each one a control the fixture offers.
116119
expect(screen.queryByRole('button', { name: /^hide fields/i })).toBeNull();

0 commit comments

Comments
 (0)