Skip to content

fix(components): 内置 textarea 全屏路径遵守 readonly/disabled — 只读不给展开按钮,禁用不可经对话框写回 (#3400) - #3401

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3400-fullscreen-readonly-disabled
Aug 5, 2026
Merged

fix(components): 内置 textarea 全屏路径遵守 readonly/disabled — 只读不给展开按钮,禁用不可经对话框写回 (#3400)#3401
yinlianghui merged 1 commit into
mainfrom
claude/issue-3400-fullscreen-readonly-disabled

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #3400

问题

renderFieldComponent 在入口就把 readonly 从 props 里解构走了,因此它不在各分支 spread 的 ...rest 里。内置 textarea 分支有两条出口,只有非全屏那条把它放了回去(readOnly={readonly} + readonlyInputClass);mobile_fullscreen 那条两样都没转发。

FullscreenTextarea 内部有三个控件 —— 内联 textarea、展开按钮、对话框里那个独立的 textarea —— 而只有第一个见过 spread 进来的 prop。于是:

  • readonly:长文本字段连对话框都不用开,内联就能直接改;
  • disabled:外观是对的(内联控件灰着),但展开按钮仍可点,对话框里随便改,点「完成」把值写回表单状态 —— 这条更难被发现,因为控件看起来是禁用的。

这不是刁钻写法:ObjectFormmobile.fullscreenLongText 打开时给每一个长文本字段无差别盖 mobile_fullscreen,不看 readonly/disabled;readonly 还可由 readonlyWhen 的 CEL 在运行时解析;disabled 又会因 isSubmitting 为真 —— 也就是提交进行中仍可通过全屏对话框改值

改法(PM 裁定的方案 2+1 组合)

  • readonly完全不渲染展开按钮。这与注册路径一致(fields/src/widgets/TextAreaField.tsx 在 readonly 时提前返回只读展示,showFullscreenButton 根本算不到),两条渲染路径对同一份元数据给出同一种用户可见行为。给个禁用的按钮反而更糟:它宣告了一个只读路径压根没有的可供性。
  • disabled ⇒ 按钮保留但 disabled(disabled 是「不可交互、变灰」,不是「平铺展示」,与调用点一贯的区分一致)。
  • 两种状态在对话框内部再守一道:对话框的 textarea 跟随 readOnly/disabled,「完成」按钮禁用,且 commit 走同一个 locked 闸门。这不是叠甲 —— disabled 同时也是 isSubmitting,它可以在对话框已经打开时翻成 true,那一刻按钮已经不是关口了。
  • 内联出口补齐 readOnly={readonly} 与只读底色;readonly 时去掉 pr-10(那段右内边距是给按钮预留的,没有按钮就是死空间)。

FullscreenTextareareadOnly / disabled 现在是声明的 props,不再是搭 rest 便车的乘客。

测试

新增 form-fullscreen-textarea-readonly-disabled.test.tsx,14 例,覆盖 issue 实测的全部状态:readonly(无展开按钮、内联不可改、只读底色、readonlyWhen 运行时解析同样成立)、disabled(按钮禁、点击也不开对话框)、isSubmitting 期间(对话框内防线 + 写回被拒)、以及正常字段全屏编辑/取消/内联编辑不回归。

反向验证(方向先声明后执行):预测「加固类改动 ⇒ 还原后守卫用例转红、对照与回归用例保持绿」。stash 掉 form.tsx 后实测 9 红 / 5 绿,且这 5 个绿正是预测点名的那 5 个 —— leaves an editable long-text field alonekeeps the inline control disabled(该出口本来就守住了,issue 自己也测到「内联 disabled = true」)、以及三条全屏往返回归用例。没有任何一例是因为「什么都没产出」而空绿。

消费半径已扫:form-textarea-fullscreen-* testid 与 mobile_fullscreen 的全仓 fixture(components 三个姊妹测试 + types + plugin-form + fields 两个注册路径测试)7 文件 43 例全绿;packages/components 全量 84 文件 614 例全绿;type-check 通过;改动文件 eslint 0 error;check:control-bytes OK。

⛔ 未触碰 FullscreenFieldEditor / TextAreaField / ObjectForm。issue 正文末那条未验证的 .field 载体镜像不在本单范围,未修也未验证。

🤖 Generated with Claude Code

https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt


Generated by Claude Code

…llscreen path (#3400)

`renderFieldComponent` destructures `readonly` off its props, so it is not in
the `...rest` each branch spreads. The built-in `textarea` branch has two exits
and only the plain one put it back (`readOnly={readonly}` + the read-only tint);
the `mobile_fullscreen` exit forwarded neither.

`FullscreenTextarea` renders three controls — the inline textarea, the expand
button, and the dialog's own textarea — and only the first ever saw a spread
prop. So a read-only long-text field was editable in place without opening the
dialog, and a disabled one looked correctly greyed out while its expand button
stayed live, its dialog accepted any edit, and "Done" wrote that edit back into
form state.

`readonly` now suppresses the expand button entirely, matching the registered
path (`fields/src/widgets/TextAreaField.tsx` early-returns a read-only display
before `showFullscreenButton` is computed), so both renderers give the same
metadata the same user-visible behaviour. `disabled` keeps the button but
disables it. Both states also lock the dialog's textarea and its "Done" button
independently of the button, because `disabled` is additionally `isSubmitting`,
which can flip to true while the dialog is already open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 5, 2026 6:08pm

Request Review

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-DUZpn7Qr.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.47KB 3.09KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 18.38KB 4.49KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 3.65KB 1.42KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.25KB 0.53KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 478.45KB 105.05KB
core (index.js) 2.25KB 0.80KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 136.23KB 34.75KB
fields (index.js) 227.18KB 55.72KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.65KB 1.06KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 9.48KB 3.27KB
i18n (useObjectLabel.js) 26.14KB 6.07KB
i18n (useSafeTranslation.js) 3.26KB 1.44KB
layout (index.js) 38.53KB 10.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.05KB 1.53KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.98KB 12.37KB
plugin-charts (index.js) 61.04KB 17.31KB
plugin-chatbot (index.js) 180.09KB 42.72KB
plugin-dashboard (index.js) 112.01KB 28.86KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 231.54KB 57.09KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 111.54KB 26.97KB
plugin-gantt (index.js) 162.55KB 39.57KB
plugin-grid (index.js) 185.08KB 49.04KB
plugin-kanban (index.js) 47.89KB 13.18KB
plugin-list (index.js) 105.02KB 25.36KB
plugin-map (index.js) 16.81KB 5.24KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.55KB 10.59KB
plugin-timeline (index.js) 25.76KB 7.33KB
plugin-tree (index.js) 8.34KB 2.82KB
plugin-view (index.js) 83.67KB 20.43KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 19.28KB 6.38KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.02KB 0.55KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 2.46KB 1.21KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 0.20KB 0.18KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Aug 6, 2026
…k-ai#3403)

* fix(fields): honour disabled on the registered fullscreen long-text path (objectstack-ai#3402)

`disabled` reached the inline control of `TextAreaField` / `RichTextField` and
nothing else. `showFullscreenButton` did not consult it, neither call site
forwarded it, and `FullscreenFieldEditor` did not declare the prop at all — so a
disabled long-text or rich-text field was greyed out inline while its expand
button stayed live, its dialog accepted any edit, and "Done" wrote that edit back
through `onCommit`.

The issue carried static evidence only, so the premise was reproduced with a
dynamic probe first: toggle `disabled=false`, dialog opened, dialog input
`disabled=false`, `onChange` called with "EDITED WHILE DISABLED" — on both
widgets, and again with `disabled` flipped true under an already-open dialog.

`FullscreenFieldEditor` now declares `disabled`, shaped like the built-in path's
`locked` gate (objectstack-ai#3400 / PR objectstack-ai#3401): the toggle stays but is disabled and refuses to
open, the dialog's editor is disabled via a new third `children` argument, and
"Done" is disabled and gated before `onCommit`. The dialog holds on its own
rather than trusting the button, because `disabled` also carries the form's
`isSubmitting` and can flip true while the dialog is open. Cancel and Esc stay
live so a submit in flight cannot trap the user in a modal.

`readonly` is untouched: both widgets early-return a read-only display before the
affordance is computed, which is why readonly was never part of this defect, and
no `readonly` prop is added here that no host would produce.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt

* test(fields): merge the stacked doc comments on the fullscreen-disabled helper

Comment-only. Two JSDoc blocks had accreted on `openThenDisable`; they say one
thing, so they are one block.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants