Skip to content

fix(MobileTokenizer): refine touch sheet editing flow - #8742

Open
rubyycheung wants to merge 1 commit into
mainfrom
ruby/mobile-tokenizer-search-border
Open

rubyycheung wants to merge 1 commit into
mainfrom
ruby/mobile-tokenizer-search-border

Conversation

@rubyycheung

@rubyycheung rubyycheung commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

User need

People editing mobile Tokenizer selections need search, selection, and dismissal to behave predictably on a phone. The existing Lab flow hid its footer while searching, committed every toggle immediately, and offered no protection against accidental dismissal.

Problem and solution fit

This change updates MobileTokenizer in Lab to own one selection draft per sheet opening and commit it only from Save. Clear all and Save stay visible while searching. A dirty swipe, scrim press, or Escape opens a discard confirmation; clean dismissals still close directly. Confirmation scrims and Keep editing return to the unchanged draft, with a destructive action followed by a full-width secondary safe action.

The large bordered search remains fixed above the results. Scrolling or dragging the results dismisses the software keyboard without clearing the query or interfering with text selection inside the search field. maxEntries is enforced against the live draft, including when the limit changes while the sheet is open.

The shared Tall BottomSheet keyboard logic also restores its pre-keyboard body position as the visual viewport recovers, including accessory-bar-only recovery, while keeping the active field visible and preserving retained layout during dismissal.

Public delta and caller burden

  • No new public prop or export.
  • Lab MobileTokenizer now:
    • stages row, Create, and Clear all changes until Save;
    • invokes onChange once with the final draft and latest change metadata;
    • keeps Clear all and Save visible during search;
    • confirms dirty swipe, scrim, and Escape dismissal;
    • returns focus to the sheet after Keep editing or confirmation-scrim dismissal;
    • closes nested confirmations if the field becomes disabled;
    • dismisses search focus only when results move;
    • honors current maxEntries before committing.
  • Nested MobilePowerSearch string-list and entity-list editors retain their staged outer flow.
  • New localized strings cover Save, discard confirmation, and a changed selection limit.

Design and alternatives

The draft model makes Save meaningful and prevents accidental loss. Confirmation actions stay stacked at full width for touch targets and translation resilience. The destructive action leads; the separated secondary action remains explicit and easy to hit.

Rejected: side-by-side confirmation actions, because they narrow touch targets and place destructive and safe actions closer together.

Evidence

  • Verified the open sheet and dirty-discard confirmation in the iPhone Simulator using the Lab MobileTokenizer Storybook story.
  • 248 focused tests pass: 29 MobileTokenizer, 103 BottomSheet, and 116 MobilePowerSearch.
  • Core and Lab builds, Lab and workspace typechecks, targeted ESLint, Storybook build, repository checks, i18n validation, sync validation, and changeset validation pass.
  • Independent review findings around focus restoration, disabled closure, dynamic limits, search gestures, and keyboard recovery have regression coverage.

Scope

  • One mobile selection-flow capability is updated.
  • Unrelated fixes, cleanup, and policy changes were excluded.
  • Consumer docs and focused tests ship with the behavior.
  • Public text and files contain no internal Meta context.

Testing

  • pnpm exec vitest run packages/lab/src/MobileTokenizer/MobileTokenizer.test.tsx packages/core/src/BottomSheet/BottomSheet.test.tsx
  • pnpm exec vitest run packages/lab/src/MobilePowerSearch/MobilePowerSearch.test.tsx
  • pnpm --filter @astryxdesign/core build
  • pnpm --filter @astryxdesign/lab build
  • pnpm typecheck
  • Targeted ESLint for all changed TypeScript files
  • pnpm check:repo
  • pnpm --filter @astryxdesign/storybook build

@vercel

vercel Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
astryx Ready Ready Preview Oct 30, 2026 4:39am UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Oct 30, 2026, 11:45 AM UTC

@github-actions github-actions Bot added needs:code-review High-risk change (new package/component/API) — needs human code review before merge needs:spec-owner-review Current knowledge records await owner approval labels Oct 27, 2026
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 27, 2026
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

Modified Components

MobilePowerSearch (@astryxdesign/lab) · View in Storybook
Metric Before After Delta
Entry File (ESM) 336B 336B 0B
Lines of Code 2748 2748 -
Complexity Very High Very High (402) -
MobileTokenizer (@astryxdesign/lab) · View in Storybook
Metric Before After Delta
Entry File (ESM) 328B 328B 0B
Lines of Code 979 1148 -
Complexity Very High Very High (147) -

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/BottomSheet 34.2KB 34.3KB +56B 7.9KB 7.9KB 0B
core/BottomSheetSwitcher 14.8KB 14.8KB 0B 3.3KB 3.3KB 0B
core/ContextMenu 75.8KB 75.9KB +100B 14.8KB 14.8KB 0B
core/DateInput 101.4KB 101.5KB +151B 14.9KB 14.9KB 0B
core/DateTimeInput 111.6KB 111.8KB +151B 15.4KB 15.4KB 0B
core/DropdownMenu 73.9KB 74.0KB +107B 15.0KB 15.0KB 0B
core/Markdown 146.0KB 146.1KB +97B 17.8KB 17.8KB 0B
core/MoreMenu 74.2KB 74.3KB +105B 15.0KB 15.0KB 0B
core/MultiSelector 74.2KB 74.3KB +96B 14.8KB 14.8KB 0B
core/Pagination 86.4KB 86.5KB +113B 15.5KB 15.5KB 0B
core/Popover 50.9KB 51.0KB +100B 12.2KB 12.2KB 0B
core/PowerSearch 168.0KB 168.1KB +161B 20.1KB 20.1KB 0B
core/Selector 78.0KB 78.1KB +103B 15.2KB 15.2KB 0B
core/Table 85.4KB 85.5KB +101B 15.6KB 15.6KB 0B
core/TableCell 77.0KB 77.1KB +104B 15.3KB 15.3KB 0B
core/TableHeaderCell 76.6KB 76.7KB +102B 15.0KB 15.0KB 0B
core/TimeInput 88.6KB 88.7KB +159B 13.9KB 13.9KB 0B
core/Tokenizer 69.8KB 69.9KB +103B 16.7KB 16.7KB 0B
lab/ChatEmojiPicker 66.7KB 66.8KB +106B 773B 773B 0B
lab/ChatReactionBar 67.3KB 67.4KB +97B 1.1KB 1.1KB 0B
lab/CommentComposer 95.7KB 95.8KB +103B 1.9KB 1.9KB 0B
lab/CommentThread 217.7KB 217.8KB +105B 2.4KB 2.4KB 0B
lab/Comments 222.2KB 222.3KB +108B 2.5KB 2.5KB 0B
lab/MobilePowerSearch 148.4KB 148.7KB +253B 1.4KB 1.4KB 0B
lab/MobileTokenizer 94.0KB 93.9KB -158B 880B 880B 0B
lab/TourStep 56.3KB 56.4KB +104B 590B 590B 0B
richtext/RichTextEditorToolbar N/A 214.1KB N/A N/A 454B N/A

Sizes are gzip. Import counts the JavaScript the import loads eagerly; CSS counts the package stylesheet rules referenced by the modules it can load from its own package, lazily loaded ones included.

Bundle Size Summary

Gzip figures measure each package's entry file, not the code an import pulls in.

Package Entry (ESM) Entry gzip before Entry gzip after Delta
@astryxdesign/core 4.9KB 1.2KB 1.2KB 0B
@astryxdesign/lab 4.6KB 1.9KB 1.9KB 0B
Package stylesheet Gzip before Gzip after Delta
@astryxdesign/core 35.4KB 35.4KB 0B
@astryxdesign/lab 11.1KB 11.1KB 0B

Accessibility Audit

Status: No accessibility violations detected.

Visual Regression

12 of 40 shot(s) changed.

A change here is a question, not a failure: check whether the after is the
picture you intended. Record that review on the PR. Baseline maintenance is an
explicit dispatch of CI; this report never rewrites the baseline or adds a release gate.

component story theme mode pixels
Selector Indicator Space Bottom Sheet Narrow End RTL neutral light 753,933
Selector Indicator Space Bottom Sheet Wide Start neutral light 740,072
Markdown Default probe light 196,790
Markdown Default probe dark 195,945
Markdown Default neutral dark 7,573
Markdown Default neutral light 7,566
Selector Indicator Space Popover Wide End RTL neutral light 204
Selector Indicator Space Popover Wide End RTL neutral dark 200
Selector Indicator Space Bottom Sheet Narrow End RTL neutral dark 153
Selector Indicator Space Bottom Sheet Wide Start neutral dark 123
Selector Indicator Space Popover Narrow Start neutral dark 93
Selector Indicator Space Popover Narrow Start neutral light 90

Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval waits on an engineering owner's decision, because this PR rewrites the component:Tokenizer touch-sheet contract (FR18, FR19, FR22–FR24 and the DEC-3 counts). An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request; the author may do so if they are an engineering owner. The owner needs to settle the draft that Save commits (including what change reports), the discard confirmation, the footer staying while searching, the bordered search field, scrim-dismissable confirmations in place of AlertDialog, dismissing the keyboard on list scroll, and BottomSheet restoring the pre-keyboard scroll. Whatever is decided, these need changes:

  • After the result list has been scrolled, typing in Search keeps only the first character, because the list's onScroll blur also fires on the scroll a keystroke causes.
  • Save reports the last draft action as change, which can name an item that never entered or left the value, so derive it from the net difference or document the behavior.
  • The branch conflicts with main, which now keeps the sheet unexposed: rebase, keep main's implementation-gap text, drop the consumer-docs edit, make the changeset internal, and credit @rubyycheung.
  • Dismissing the keyboard on scroll needs agreement in spec:AST-094 and spec:AST-099, plus the iOS Simulator evidence spec:AST-099 asks for.

[Automated review]

focusedSearch != null &&
document.activeElement === focusedSearch
) {
focusedSearch.blur();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This blur also runs for scrolls the person did not make. When a keystroke shrinks a scrolled list, the browser clamps scrollTop and fires scroll, so the field loses focus after the first character: in Chromium, typing "Eng" after scrolling the list leaves "E". Blurring only from the person's own drag (for example touchmove past a small threshold) would avoid it; a test that types after a scrolled list would catch it.

? ({item: fallbackRemoved, type: 'remove'} as const)
: null);
if (change != null) {
onCommit([...draftValue], change);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change here is the last draft action. Opening with Design and Eng, then checking Energizer, unchecking Design and unchecking Energizer reports onChange([Eng], {item: Energizer, type: 'remove'}), although only Design left the value. Deriving change from the net difference between valueAtOpen and draftValue would keep the docs' "affected item" true.

'@astryxdesign/core': patch
---

[experimental] Improve Tokenizer's touch sheet with a bordered search field, persistent footer actions, keyboard dismissal while results scroll, draft selections committed by Save, and confirmation before a dirty draft is discarded.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

main now ships the sheet presentation unexposed, with an internal [chore] changeset. After the rebase, this note should be internal too, not a consumer-facing [experimental] entry.


[experimental] Improve Tokenizer's touch sheet with a bordered search field, persistent footer actions, keyboard dismissal while results scroll, draft selections committed by Save, and confirmation before a dirty draft is discarded.

@rubycheung

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This handle is @rubycheung; your other changeset uses @rubyycheung, and the changelog credits the handle written here.

@rubyycheung
rubyycheung force-pushed the ruby/mobile-tokenizer-search-border branch from ccf3642 to 0d1c584 Compare October 28, 2026 18:02
@rubyycheung rubyycheung changed the title fix(Tokenizer): refine touch sheet editing flow fix(MobileTokenizer): refine touch sheet editing flow Oct 28, 2026
@rubyycheung
rubyycheung force-pushed the ruby/mobile-tokenizer-search-border branch from 0d1c584 to f8bc6c5 Compare October 28, 2026 18:05
@rubyycheung
rubyycheung marked this pull request as ready for review October 28, 2026 18:10
@rubyycheung
rubyycheung force-pushed the ruby/mobile-tokenizer-search-border branch from f8bc6c5 to 0e0ac7c Compare October 28, 2026 18:19
@rubyycheung rubyycheung reopened this Oct 28, 2026
@rubyycheung
rubyycheung force-pushed the ruby/mobile-tokenizer-search-border branch from 0e0ac7c to 34da74b Compare October 28, 2026 18:20
@rubyycheung
rubyycheung force-pushed the ruby/mobile-tokenizer-search-border branch from 34da74b to a40cf71 Compare October 28, 2026 22:43
@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Oct 28, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval waits on an engineering owner's decision about the Core BottomSheet change: returning a Tall sheet's body to its pre-keyboard scroll position as the keyboard leaves, including after an iOS dismissal that leaves only the accessory bar, is not stated in the on-screen keyboard spec (spec:AST-099), so an engineering owner needs to settle that behavior and record the decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so if they are an engineering owner). Independently of that decision, please fix the points below and rebase onto main, which no longer embeds MobileTokenizer in MobilePowerSearch, so the MobilePowerSearch.test.tsx edits drop out:

  • Every scroll event on the results list blurs the search, so after the list has scrolled, typing keeps only the first character (MobileTokenizer.tsx:1169–1171).
  • A keyboard that only gets shorter while a field keeps focus resets a Tall sheet's body to its opening scroll and glides it back (useMobileKeyboard.ts:338–346).
  • Re-checking an item selected at open counts as a change, and change reports only the last draft action, which can name an item that never joined or left the value (MobileTokenizer.tsx:142–146, :1098–1104).
  • The new layout effect sets state when the sheet closes (:1038–1046), the changeset credits @rubycheung instead of @rubyycheung, and spec:AST-099 asks for iOS Simulator evidence in a top-level document before keyboard work merges.

[Automated review]

data-total-count={visibleItems.length}
onTouchMove={blurSearch}
onScroll={event => {
blurSearch();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This blur also runs for scrolls the person did not make. When a keystroke shrinks a scrolled list (the filter, plus the window reset in handleQueryChange), the browser clamps scrollTop and fires scroll, so the search loses focus after the first character: in Chromium, typing "Eng" after scrolling the list leaves "E". Blurring only on the person's own drag of the list (a touchmove past a small threshold after a touchstart on the list) would keep typing intact.

obstructionTop > keyboardObstructionTop + 0.5 &&
bodyScrollTopAtKeyboard != null
) {
body.scrollTop = bodyScrollTopAtKeyboard;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any growth of the visual viewport lands here, not only an interactive dismissal: a keyboard that gets shorter while the field keeps focus (predictive bar hiding, a keyboard switch, Next to a field with a shorter keyboard) resets the body to its opening scroll, then the safe-area pass smooth-scrolls back. With the field at 660–700px and the keyboard top moving from 500 to 540px, the body goes 248 → 0 → 208. Could the restore be gated on a signal that identifies the dismissal, with a test where the keyboard shrinks and the field stays focused?

item => !draftValue.some(draftItem => draftItem.id === item.id),
);
const change =
lastDraftChangeRef.current ??

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change here is the last draft action, not the net change. Opening with Design and Eng, then checking Energizer, unchecking Design and unchecking Energizer reports onChange([Eng], {item: Energizer, type: 'remove'}), although only Design left the value; unchecking and re-checking Design reports an add for an item the caller already holds. Deriving change from the difference between valueAtOpen and the draft (and comparing the draft as a set, or restoring a re-checked item's opening position) would keep it truthful.

shouldRestoreDraftFocusRef.current = true;
setIsDiscardConfirmationOpen(false);
};
useLayoutEffect(() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This layout effect sets state after the commit that force-closes the sheet, so that commit still renders an open confirmation. architecture:react-component-runtime INV1 and INV3 apply to packages/lab/src/; closing both confirmations in the existing isOpen !== wasOpen render adjustment, or in the close path, avoids the repair, and the focus effects below would each need a comment naming the dialog close they follow.


[fix] Fix Tall BottomSheet keyboard layout recovery when iOS interactively dismisses the software keyboard.

@rubycheung

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This handle is @rubycheung; your other changesets use @rubyycheung, and the changelog credits the handle written here.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval is waiting on an engineering owner's decision about the Core BottomSheet change: the on-screen keyboard spec (spec:AST-099) does not say that a Tall sheet's body returns to its pre-keyboard scroll position as the keyboard leaves, so an engineering owner needs to identify or provide the governing spec for that behavior, including the iOS case where only the accessory bar remains, and record the decision by commenting /approve-spec <full head SHA> on this pull request (the author may do this if they are an engineering owner). Independently of that decision, these points need changes:

  • Every scroll event on the results list blurs the search, including the one a keystroke causes, so typing after the list has scrolled stops after one character (MobileTokenizer.tsx:1169–1171).
  • Re-checking an item counts as an edit, and Save reports only the latest draft action as change, which can name an item that never joined or left the value (MobileTokenizer.tsx:142–146, :1098–1104).
  • A keyboard that only gets shorter while a field stays focused snaps a Tall sheet's body to its opening scroll before gliding back (useMobileKeyboard.ts:338–346).
  • The layout effect at MobileTokenizer.tsx:1038–1046 sets state when the sheet closes, the changeset credits @rubycheung rather than @rubyycheung, and spec:AST-099 asks for iOS Simulator evidence in a top-level document before keyboard work merges.

[Automated review]

@rubyycheung
rubyycheung enabled auto-merge (squash) October 29, 2026 05:31

This branch was successfully deployed

1 active (outdated) deployment
Preview — f90a9b53 Deployed Oct 30, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. needs:code-review High-risk change (new package/component/API) — needs human code review before merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant