Repository navigation
Commit 39951d2
fix(fields, plugin-form): form controls reach a 44 px touch target at phone width (#29209)
domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} -->
_Requested by **Steedos** · [project
thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbNADoD69X24js3pV37rARs6)_
Fixes #29072
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:手机宽度(低于 `sm`,640 px)下,新建 /
编辑表单里的输入框、下拉、查找字段、日期选择器、明细行表格和表单底部按钮,触控区域统一达到 44 px;桌面宽度外观不变。
- **为什么改**:移动端 App 用 WebView 承载 Console(#29040),实测 375 / 390 px 下表单控件只有
28–40 px,日历格子和下拉选项 32 px,低于 44 px 触控下限,容易点错。
- **风险与回滚**:只加 Tailwind 类(`min-h-[44px]
sm:min-h-0`,与表单内置控件已用的同一个记号),不改逻辑、不改导出、不改元数据;回滚即 revert 本 PR。
-
**先看**:`ui/packages/fields/src/widgets/date-picker/DatePickerField.tsx`(右侧按钮变大后输入框右内边距改为
`pr-24 sm:pr-16`、日历格子 44
px);`ui/packages/fields/src/widgets/LookupField.tsx`;`ui/packages/fields/src/widgets/touchTarget.ts`。
- **两分钟验证**:手机尺寸打开 CRM「商机 → 新建」,点开「预计成交日期」日历:每个日期格子约 44
px,一周七天仍完整放在屏幕内;桌面宽度下同一日历仍是 32 px 格子。
- **需要你做的**:无,席位落地。
## What changes
At phone width (below `sm`), the create / edit form's registered field
widgets now carry the same touch floor the form renderer's built-in
controls already use, `min-h-[44px] sm:min-h-0`, through a
package-internal token module
`ui/packages/fields/src/widgets/touchTarget.ts` (not re-exported, so no
published surface moves). From `sm` up every control keeps its previous
height.
- **Inputs:** text, email, phone, URL, number, currency and percent
inputs; the percent slider's hit area (its root takes the pointer along
its whole height).
- **Select:** the trigger and every option.
- **Lookup:** the trigger, the clear and browse-all buttons, the
dropdown's search box, its "show all results" and quick-create buttons;
the record picker dialog's search box and Filters button.
- **Date picker:** the input, its clear and calendar buttons, the Today
/ Tomorrow shortcuts, and a 44 px calendar cell below `sm`
(`[--cell-size:2.75rem] sm:[--cell-size:2rem]` passed to the `Calendar`
primitive, which `cn` merges over its default). Seven 44 px days plus
the calendar padding measure 332 px, inside a 343 px form column at 375
px. The input's right padding grows to `pr-24` below `sm` so its text
stops short of the two 44 px buttons.
- **Line-item grid** (opportunity products): cell inputs, cell selects
and their options, the compact lookup cell, the three row buttons, and
"Add line".
- **Form footers:** the modal, drawer and master-detail forms' Cancel /
Reset / Submit buttons.
- **File field** (added from #29071's run, on the seat's order): the
"Take photo" camera button gets `PHONE_TOUCH_HEIGHT` and the per-file
remove button `PHONE_TOUCH_SQUARE` in `FileField.tsx`. Their before
sizes, 36 px (`Button` `size="sm"`, `h-9`) and 24×24 (`h-6 w-6`), were
read from the `Button` token and the class, not measured on a page; no
create form in either example drew a file field on this flow.
Fence check: `FormPage.tsx` is not touched (its refusal path stays with
#29019), nor `sectionFields.ts` (#29021), `sheet.tsx` (#29078),
`dialog.tsx` (#29107), `navigation-overlay.tsx`, `record-list-face.tsx`,
`ObjectView.tsx`, `recordOpenPreference.ts`, `ui/packages/mobile/**`,
the console's `main` / `App` / `AppContent`, or any shared chrome. No
`ui/packages/components/src/ui/**` primitive is edited: every change is
a className handed to it.
## Run record
Driven in Chromium (Playwright, `isMobile`, `hasTouch`, DPR 2) at
**375×812** and **390×844**, against the Console served from source
(Vite) over `examples/app-crm` and `examples/app-showcase` started with
`objectstack dev --seed-admin --fresh`. Flow per object: list → phone
create button → create form → select listbox → date picker (where the
object has one) → lookup dropdown → browse-all record picker → keyboard
check on the last and a middle text control → clear the required field
and submit (validation) → open a seeded record → Edit form. **Before** =
merge base `a4015ab4` (the widget sources restored from it), **after** =
this branch head.
| Check | Example · page | 375×812 before | 375×812 after | 390×844
before | 390×844 after |
|:--|:--|:--|:--|:--|:--|
| 1 No horizontal page scroll | CRM · lead create + edit | pass | pass |
pass | pass |
| 1 No horizontal page scroll | CRM · opportunity create + edit | pass |
pass | pass | pass |
| 1 No horizontal page scroll | Showcase · task create + edit | pass |
pass | pass | pass |
| 2 Touch targets ≥ 44 px | CRM · lead create + edit | **fail**: 15
controls at 40 px, 6 options at 32, lookup search 32 / quick-create 36,
picker search 40 / Filters 36 / Close 16 | **fail**: picker Close 16×16
only (finding, #29107) | **fail**: same as 375 | **fail**: picker Close
16×16 only |
| 2 Touch targets ≥ 44 px | CRM · opportunity create + edit | **fail**:
controls at 40, date buttons 28, line-item cells 32, Add line 36, slider
8, 5 options at 32, 39 calendar cells at 32, lookup and picker as above
| **fail**: picker Close 16×16 only | **fail**: same as 375 | **fail**:
picker Close 16×16 only |
| 2 Touch targets ≥ 44 px | Showcase · task create + edit | **fail**:
controls at 40, date buttons 28, 5 options at 32, 39 calendar cells at
32, lookup and picker as above | **fail**: picker Close 16×16 only |
**fail**: same as 375 | **fail**: picker Close 16×16 only |
| 3 Dialogs and popovers fit the viewport | all three (form dialog,
select listbox, date popover, lookup dropdown, record picker) | pass |
pass | pass | pass |
| 4 Virtual keyboard leaves the focused field visible | all three (last
and middle text control) | pass | pass | pass | pass |
| Validation: first error focused and in view | all three | pass | pass
| pass | pass |
| 5 Long lists responsive at 4× CPU | CRM · opportunity create, Account
lookup over 303 accounts, **production bundle** | pass (open 176 ms,
typing 64, scroll no long task) | borderline (open 184–256 ms over 3
runs, typing ≤ 88, scroll ≤ 51 ms tasks) | pass (open 192, typing 72) |
pass (open 192–256, typing ≤ 88) |
<details><summary>How each check was measured</summary>
- **Horizontal scroll:** `document.documentElement.scrollWidth` against
`innerWidth`, and every element in the open dialog whose box crosses the
viewport edge without an `overflow-x` ancestor clipping it (the
line-item table and the picker table scroll inside their own containers,
which is by design).
- **Touch targets:** every visible `button`, `input`, `select`,
`textarea`, `[role=combobox]`, `[role=option]`, `[role=checkbox]`,
`[role=switch]`, `a[href]` and the percent slider root inside the open
dialog, listbox or popover, by bounding box, against 44 px on both axes.
- **Fit:** the open dialog / listbox / popover box inside the viewport.
- **Keyboard:** the WebView `adjustResize` model: focus the control,
shrink the layout viewport by 336 px (a Gboard-height keyboard on an 812
px screen), let the browser scroll the focused editable into view, then
assert its box is inside the shortened viewport and `elementFromPoint`
at its centre hits it (no sticky footer or header over it). A real
device IME was not exercised.
- **4× CPU:** CDP `Emulation.setCPUThrottlingRate` 4 on the production
Console bundle (`pnpm console:build`, served at `/_console` by
`objectstack dev`), with 300 extra accounts seeded through the data API.
Pass = every Event Timing entry (click, key) at most 200 ms (the "good"
INP line), and no long task over 200 ms while scrolling. The dev-server
(unbundled React) numbers the harness also prints are 2–3× slower and
are not the reading.
- Screenshots for every step, before and after, are in the project
folder (not attached here).
</details>
## Tests
-
`ui/packages/fields/src/widgets/touchTarget.formControls-29072.test.tsx`
(14 cases): the token shape, each input widget, a host className still
reaching the input beside the token, the slider root, the select trigger
and options, the lookup trigger / clear / browse-all, the date picker's
input, buttons and calendar cell size, and the file field's camera and
remove buttons. Reverse-verified: with the widget sources restored from
the merge base, 12 of 13 go red (the token-shape case reads only the new
module).
- `ui/e2e/form-controls-phone-touch.spec.ts` (3 cases, shape of
`record-path-phone-row.spec.ts`): reads the tokens, the `Input` /
icon-`Button` base heights and the calendar's cell-size classes out of
the sources, compiles real Tailwind for them, and measures a fixture: at
375 and 390 px the input, the date-picker button, the browse-all button
and all seven calendar days are at least 44 px and the calendar ends
inside the form column with no page scroll; at 1024 px the primitives
keep 40 / 28 / 40 / 32 px. Red on the merge base (the date picker
carries no cell-size override there).
## Acceptance notes
- **Finding (dialog close, carrier #29107):** the lookup's browse-all
record picker uses the plain `DialogContent`, whose Close button
measures 16×16 px at both widths (CRM lead and opportunity Account
picker, showcase task Project picker). `dialog.tsx` is #29107's file, so
it is left as is here.
- **Finding (borderline, 4× CPU):** opening the Account lookup dropdown
on the production bundle with 303 accounts takes one 150–215 ms task;
the click's Event Timing duration measured 176 and 192 ms before this
change and 184–256 ms over three runs after it (two of six samples over
200 ms). This diff adds classes only, so the cost predates it; typing
and scrolling stay under 100 ms.
- **Observation:** the opportunity form (master-detail) keeps its Cancel
/ Create bar at the end of the scrolling body, unlike the modal form's
sticky footer, so at 375×812 the Create button is not on the first
screen. None of the five checks covers it; noted for the seat.
- The flow's create and edit surfaces at phone width are the full-screen
form dialog for all three objects; no object in either example declares
the page or drawer form mode, so `RecordFormPage` was not reached. The
drawer form's footer gets the same token for parity.
## Verification
At head `99a5359b` (main merged at `dedc3505`; main touched none of
these files). The file-field commit was re-verified on its own: the
touchTarget test 14 / 14 (its new file case red with `FileField.tsx`
from the previous head), the touched test files plus
`complex-widgets.test.tsx` 110 files / 851 tests, the
`@object-ui/fields` type-check 47 tasks exit 0, ESLint on the file 0
errors, `check-sdui-manifest` 0. The readings below were taken at
`6034e7ec`:
- `node scripts/pm/dispatch-gates.mjs --ran`: 34 derived, 34 run, all
exit 0. `pnpm --dir ui exec vitest run scripts/` was killed once by
memory pressure while the run-record servers were up; rerun alone it
passes (169 files, 4900 tests).
- `node scripts/check-sdui-manifest.mjs`: exit 0, after the merge.
- Type-check of `@object-ui/fields` and `@object-ui/plugin-form` (turbo,
49 tasks): exit 0. ESLint on the changed files: 0 errors.
- The new unit file (13 at `6034e7ec`, 14 now) and the touched widgets'
and form footers' existing test files: 106 files, 768 tests, pass after
the merge. The e2e spec: 3 pass.
- Diff at `99a5359b`: 20 files, +405 / −38, well under 3000 lines. No
exported type of a published package moves.
The os-dev report on #29072 carries the same readings.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QsR2jUqXiWNJKjMJYThRr1)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 9f120af commit 39951d2
20 files changed
Lines changed: 405 additions & 38 deletions
File tree
- .changeset
- ui
- e2e
- packages
- fields/src/widgets
- date-picker
- plugin-form/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
5 | 6 | | |
6 | 7 | | |
7 | 8 | | |
| |||
190 | 191 | | |
191 | 192 | | |
192 | 193 | | |
193 | | - | |
| 194 | + | |
194 | 195 | | |
195 | 196 | | |
196 | 197 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | | - | |
| 2 | + | |
| 3 | + | |
3 | 4 | | |
4 | 5 | | |
5 | 6 | | |
| |||
27 | 28 | | |
28 | 29 | | |
29 | 30 | | |
| 31 | + | |
30 | 32 | | |
31 | 33 | | |
32 | 34 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | | - | |
| 2 | + | |
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| 9 | + | |
9 | 10 | | |
10 | 11 | | |
11 | 12 | | |
| |||
344 | 345 | | |
345 | 346 | | |
346 | 347 | | |
347 | | - | |
| 348 | + | |
348 | 349 | | |
349 | 350 | | |
350 | 351 | | |
| |||
430 | 431 | | |
431 | 432 | | |
432 | 433 | | |
433 | | - | |
| 434 | + | |
434 | 435 | | |
435 | 436 | | |
436 | 437 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| 28 | + | |
28 | 29 | | |
29 | 30 | | |
30 | 31 | | |
| |||
1246 | 1247 | | |
1247 | 1248 | | |
1248 | 1249 | | |
1249 | | - | |
| 1250 | + | |
1250 | 1251 | | |
1251 | 1252 | | |
1252 | 1253 | | |
1253 | 1254 | | |
1254 | 1255 | | |
1255 | 1256 | | |
1256 | 1257 | | |
1257 | | - | |
| 1258 | + | |
1258 | 1259 | | |
1259 | 1260 | | |
1260 | 1261 | | |
| |||
1284 | 1285 | | |
1285 | 1286 | | |
1286 | 1287 | | |
| 1288 | + | |
1287 | 1289 | | |
1288 | 1290 | | |
1289 | 1291 | | |
| |||
1471 | 1473 | | |
1472 | 1474 | | |
1473 | 1475 | | |
1474 | | - | |
| 1476 | + | |
1475 | 1477 | | |
1476 | 1478 | | |
1477 | 1479 | | |
| |||
1489 | 1491 | | |
1490 | 1492 | | |
1491 | 1493 | | |
1492 | | - | |
| 1494 | + | |
1493 | 1495 | | |
1494 | 1496 | | |
1495 | 1497 | | |
| |||
1505 | 1507 | | |
1506 | 1508 | | |
1507 | 1509 | | |
1508 | | - | |
| 1510 | + | |
1509 | 1511 | | |
1510 | 1512 | | |
1511 | 1513 | | |
| |||
1548 | 1550 | | |
1549 | 1551 | | |
1550 | 1552 | | |
| 1553 | + | |
1551 | 1554 | | |
1552 | 1555 | | |
1553 | 1556 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| 15 | + | |
15 | 16 | | |
16 | 17 | | |
17 | 18 | | |
| |||
1604 | 1605 | | |
1605 | 1606 | | |
1606 | 1607 | | |
| 1608 | + | |
1607 | 1609 | | |
1608 | 1610 | | |
1609 | 1611 | | |
| |||
1682 | 1684 | | |
1683 | 1685 | | |
1684 | 1686 | | |
1685 | | - | |
| 1687 | + | |
1686 | 1688 | | |
1687 | 1689 | | |
1688 | 1690 | | |
| |||
1801 | 1803 | | |
1802 | 1804 | | |
1803 | 1805 | | |
1804 | | - | |
| 1806 | + | |
1805 | 1807 | | |
1806 | 1808 | | |
1807 | 1809 | | |
| |||
1855 | 1857 | | |
1856 | 1858 | | |
1857 | 1859 | | |
1858 | | - | |
| 1860 | + | |
1859 | 1861 | | |
1860 | 1862 | | |
1861 | 1863 | | |
| |||
1932 | 1934 | | |
1933 | 1935 | | |
1934 | 1936 | | |
1935 | | - | |
| 1937 | + | |
1936 | 1938 | | |
1937 | 1939 | | |
1938 | 1940 | | |
| |||
1947 | 1949 | | |
1948 | 1950 | | |
1949 | 1951 | | |
1950 | | - | |
| 1952 | + | |
1951 | 1953 | | |
1952 | 1954 | | |
1953 | 1955 | | |
| |||
1970 | 1972 | | |
1971 | 1973 | | |
1972 | 1974 | | |
1973 | | - | |
| 1975 | + | |
1974 | 1976 | | |
1975 | 1977 | | |
1976 | 1978 | | |
| |||
0 commit comments