Repository navigation
Commit 2f380dd
fix(components): dropdown-menu rows and tabs reach a 44 px touch target at phone width (#29348)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} -->
_Requested by **Steedos** · [project
thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9Ubb6rhJS7ZbYPhkr8QGvprAUj)_
Fixes #29215
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:手机宽度(小于 sm 断点)下,所有下拉菜单的每一行(普通项、勾选项、单选项、子菜单入口)和所有 Tabs
标签页按钮的点击高度至少 44 px,Tabs 外框随之长高;桌面宽度下样式一字未改。
- **为什么改**:移动端 App 用 WebView 承载
Console,记录页「更多」菜单、列表行操作菜单和各处标签页都用这两个共享组件,原来菜单行 32 px、标签页 32 到 38
px,手机上容易点错。
- **风险与回滚**:只加了 `max-sm:` 前缀的样式类,桌面不受影响;手机上带固定高度的标签栏(如 AI
对话页的紧凑切换器)会变高。回滚即 revert 本 PR。
-
**先看**:`ui/scripts/shadcn-local-patches.mjs`(新增的两组补丁声明)、`ui/packages/components/src/ui/tabs.tsx`(TabsList
与 TabsTrigger
两行)、`ui/packages/components/src/__tests__/menu-tabs-touch-target-29215.test.tsx`。
- **两分钟验证**:`pnpm --dir ui exec vitest run
packages/components/src/__tests__/menu-tabs-touch-target-29215.test.tsx
scripts/__tests__/shadcn-local-patches.test.ts`,预期全绿;或在 375 px 宽的
Console 打开任一记录页点「More actions」,每行高 44 px。
- **需要你做的**:无,席位落地。
## What changes
- Below `sm`, every `DropdownMenu` row is at least 44 px tall:
`DropdownMenuItem`, `DropdownMenuCheckboxItem`, `DropdownMenuRadioItem`
and `DropdownMenuSubTrigger` each gain `max-sm:min-h-11` right after
their `py-1.5`. `DropdownMenuSubTrigger` is included because a phone
user taps it to open the sub-menu; `DropdownMenuLabel` is not a control
and is left alone.
- Below `sm`, `TabsTrigger` gains `max-sm:min-h-11`, and `TabsList`
gains `max-sm:h-auto` at the end of its class string, so the list drops
its fixed `h-10` there and grows with its triggers.
- From `sm` up, every class list is upstream's, token for token. No
export, prop or `TabsContent` change.
- `dropdown-menu.tsx` and `tabs.tsx` are Shadcn-synced (`ui/AGENTS.md`
commandment 7), so both edits are declared in
`ui/scripts/shadcn-local-patches.mjs` as two new families,
`dropdownMenuRowTouchTargetPatches` and `tabsTouchTargetPatches`, built
by one small `phoneTouchTargetPatch` helper that inserts the phone class
after a named token inside a registry anchor. The shipped files were
written by applying those patches, and `pnpm shadcn:update` re-applies
them. The checkbox and radio items share one upstream class string, so
their anchor declares two occurrences.
- Both families are new to `LOCAL_PATCHES`, so the registry provenance
test requires vendored fixtures:
`ui/scripts/__tests__/fixtures/shadcn-registry/dropdown-menu.registry.txt`
and `tabs.registry.txt`, captured verbatim from shadcn-ui/ui
`8a7701ec27eb9cb8e0377db769fbe6d744113c52` through
`raw.githubusercontent.com`. The same capture reproduces `dialog`'s
recorded sha256 byte for byte. `tabs` round-trips byte for byte
(`roundTrip: true`). `dropdown-menu` cannot, because it already ships
the undeclared Tailwind v4 `origin-(--radix-…)` spelling that
`shadcn-components.json` documents, so its entry pins the inequality
with that reason, as `calendar` does.
- `ui/packages/components/shadcn-components.json`: the `dropdown-menu`
`localEdits` note names the new patch beside the Tailwind v4 note, and
`tabs` gains a `localEdits` note.
- Changeset `.changeset/29215-menu-tabs-phone-touch-target.md`:
`@object-ui/components` patch.
## Tests
- New
`ui/packages/components/src/__tests__/menu-tabs-touch-target-29215.test.tsx`
(10 cases). For each of `DropdownMenuItem`, `DropdownMenuCheckboxItem`,
`DropdownMenuRadioItem`, `DropdownMenuSubTrigger`, `TabsTrigger` and
`TabsList`, the rendered element carries exactly the one phone class,
and every other token equals upstream's list in order. A label and
`TabsContent` get no phone class. A caller's own `py-2`, or `h-7` and
`h-6`, still merge through `cn`, and the phone class survives them.
- `ui/scripts/__tests__/shadcn-local-patches.test.ts`: provenance
entries for the two fixtures, plus a new describe block. It checks the
declared ids. Applied to registry bytes, each family adds only `max-sm:`
classes, leaves every class string's other tokens identical in order,
and adds exactly 4 `max-sm:min-h-11` for dropdown-menu and 1
`max-sm:min-h-11` plus 1 `max-sm:h-auto` for tabs. Only the four menu
rows carry the floor, and `TabsList` still opens with `inline-flex h-10`
(the opening `e2e/approvals-inbox-phone.spec.ts` reads it by). The
family refuses when upstream re-pads the item (found 0x), and when the
checkbox and radio rows stop sharing a class string (found 1x).
-
`ui/packages/components/src/__tests__/__snapshots__/snapshot-critical.test.tsx.snap`:
the two "Tabs snapshots" regenerated with `vitest --update`. The diff is
the `TabsList` and `TabsTrigger` class attributes only, gaining
`max-sm:h-auto` and `max-sm:min-h-11`. A sweep of every `*.snap`,
`*.test.*` and `*.spec.*` file for the upstream item, trigger and list
class strings finds no other pin.
- Ablation, one-shot, at 43e61db, through
`scripts/ablation-replace.mjs` (anchor 1 to 0, restore verified `git
diff HEAD` empty): with `max-sm:min-h-11` dropped from
`DropdownMenuItem` in the shipped file, 3 cases go red. They are
"DropdownMenuItem: at least 44 px below sm, upstream classes from sm
up", "a caller's own padding still merges, and the phone floor survives
it" and "dropdown-menu.tsx carries its declared patches".
## Acceptance notes
- **Token choice.** The card asked for `min-h-[44px] sm:min-h-0`. This
PR uses `max-sm:min-h-11` and `max-sm:h-auto` instead, accepted by the
seat before the build. It follows the shadcn-patch precedent on main
(the Sheet close and Dialog close families are `max-sm:` only). It
leaves every desktop token byte-identical to upstream, which the tests
assert token for token. `sm:min-h-0` would also change desktop
`min-height` from `auto` to `0`, which matters in a `flex-col` tab list
(the vertical `tabs` renderer, `ViewSwitcher`'s vertical orientation).
The helper #29072 landed (`touchTarget.ts`) is internal to
`@object-ui/fields`, so `@object-ui/components` cannot import it either
way.
- **Fixed-height callers grow on phones.** A `TabsList` or `TabsTrigger`
given a fixed height by its caller still gets the phone floor, because
`max-sm:` classes do not conflict with unprefixed ones in
`tailwind-merge`. The AI chat page's compact assistant switcher (`h-7`
list, `h-6` triggers) is therefore 45 px tall below `sm`. That is the
card's intent: every tab strip.
- **Per-page phone arms now overlap the primitive floor.**
`ApprovalsInboxPage` (`tabTouch`), `InboxPopover` (`TAB_TRIGGER_CLASS`)
and `AppHeader` (`max-md:[&_[role=menuitem]]:min-h-11`) already lift
their own tabs or menu rows below `md`. They still own 640 to 767 px, so
nothing is removed here.
- **Not covered, same shape, not measured.** `ContextMenu`, `Menubar`,
`Select` and `Command` rows have the same `py-1.5` shape upstream. This
card names only DropdownMenu and Tabs.
## Verification
- Real browser: app-crm backend (`objectstack dev --seed-admin --fresh`)
plus the Console Vite dev server on source, Playwright, CRM account
record page (`/apps/crm_app/crm_account/record/:id`). Before means
main's `dropdown-menu.tsx` and `tabs.tsx`, with Vite restarted with
`--force` before the leg; the files were restored from HEAD afterwards
and `git status` was clean.
- "More actions" menu rows (Refresh, Add to favorites, Share, Delete):
375 x 812, 32 px before, 44 px after. 1280 x 800, 32 px before and
after.
- The tab row's "+3" overflow menu rows (Contacts, Opportunities,
Leads), 375 x 812: 32 px before, 44 px after.
- `TabsTrigger` in real Chromium with real Tailwind (the installed
`tailwindcss`, compiled for the class strings read out of each
`tabs.tsx`): 375 px, trigger 32 to 44 px and list 40 to 52 px. 1280 px,
trigger 32 px and list 40 px in both legs.
- The record page's own tab row (Overview, Activity, …) is not this
primitive. It is `RecordTabRow` in `@object-ui/plugin-detail`, an `h-9`
button strip, so it measures 36 px in both legs and both widths. It is
reported to the seat as a separate finding and is not changed here.
- Local gates at d67694b (main merged after #29204 landed): the 36
families `dispatch-gates --commands` derives for this diff all exit 0,
and the `--ran` reconciliation reads 36 derived, 36 run, 0 NOT-MEASURED.
They include `node scripts/check-sdui-manifest.mjs` and `pnpm --dir ui
exec vitest run scripts/` (169 files, 4921 tests passed).
`@object-ui/components` type-check (with `tsconfig.test.json`), `pnpm
--dir ui run type-check:scripts`, eslint on the five touched source and
test files and `pnpm check:new-line-citations` (0 new) all exit 0,
measured at 43e61db.
- At c25c35b (main merged again, no incoming commit touches these 10
paths): the four touch-target component tests, `snapshot-critical`, the
patch registry test and main's new `phoneCardRowActions-29150` re-run
green (6 files, 111 tests), and `check-sdui-manifest` exits 0. The whole
components package is left to CI Test UI.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [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: its generated artifacts are
regenerated and committed.
- [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.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01QsR2jSMMkBJxrdeZwTifs8
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QsR2jSMMkBJxrdeZwTifs8)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 3ff32c4 commit 2f380dd
10 files changed
Lines changed: 719 additions & 15 deletions
File tree
- .changeset
- ui
- packages/components
- src
- __tests__
- __snapshots__
- ui
- scripts
- __tests__
- fixtures/shadcn-registry
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
146 | 146 | | |
147 | 147 | | |
148 | 148 | | |
149 | | - | |
| 149 | + | |
150 | 150 | | |
151 | 151 | | |
152 | 152 | | |
| |||
316 | 316 | | |
317 | 317 | | |
318 | 318 | | |
319 | | - | |
| 319 | + | |
| 320 | + | |
320 | 321 | | |
321 | 322 | | |
322 | 323 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
647 | 647 | | |
648 | 648 | | |
649 | 649 | | |
650 | | - | |
| 650 | + | |
651 | 651 | | |
652 | 652 | | |
653 | 653 | | |
| |||
656 | 656 | | |
657 | 657 | | |
658 | 658 | | |
659 | | - | |
| 659 | + | |
660 | 660 | | |
661 | 661 | | |
662 | 662 | | |
| |||
670 | 670 | | |
671 | 671 | | |
672 | 672 | | |
673 | | - | |
| 673 | + | |
674 | 674 | | |
675 | 675 | | |
676 | 676 | | |
| |||
684 | 684 | | |
685 | 685 | | |
686 | 686 | | |
687 | | - | |
| 687 | + | |
688 | 688 | | |
689 | 689 | | |
690 | 690 | | |
| |||
738 | 738 | | |
739 | 739 | | |
740 | 740 | | |
741 | | - | |
| 741 | + | |
742 | 742 | | |
743 | 743 | | |
744 | 744 | | |
| |||
747 | 747 | | |
748 | 748 | | |
749 | 749 | | |
750 | | - | |
| 750 | + | |
751 | 751 | | |
752 | 752 | | |
753 | 753 | | |
| |||
761 | 761 | | |
762 | 762 | | |
763 | 763 | | |
764 | | - | |
| 764 | + | |
765 | 765 | | |
766 | 766 | | |
767 | 767 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
0 commit comments