Skip to content

feat[mobile]: make share menu nice#2781

Open
peterchinman wants to merge 6 commits intomainfrom
peter/mobile-dialogs
Open

feat[mobile]: make share menu nice#2781
peterchinman wants to merge 6 commits intomainfrom
peter/mobile-dialogs

Conversation

@peterchinman
Copy link
Copy Markdown
Contributor

No description provided.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

Warning

Rate limit exceeded

@peterchinman has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 10 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 9 minutes and 10 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a830de68-6982-4ee7-81c4-5863f08da47e

📥 Commits

Reviewing files that changed from the base of the PR and between 9159034 and b7fea99.

⛔ Files ignored due to path filters (1)
  • js/bun.lock is excluded by !**/*.lock, !**/bun.lock
📒 Files selected for processing (18)
  • js/app/packages/app/component/ResponsiveBlockToolbar.tsx
  • js/app/packages/app/component/mobile/MobileDrawer.tsx
  • js/app/packages/app/component/next-soup/soup-view/SoupEntityActionDrawer.tsx
  • js/app/packages/app/component/split-layout/components/SplitFileMenu.tsx
  • js/app/packages/block-canvas/component/TopBar.tsx
  • js/app/packages/block-chat/component/TopBar.tsx
  • js/app/packages/block-code/component/TopBar.tsx
  • js/app/packages/block-email/component/TopBar.tsx
  • js/app/packages/block-image/component/TopBar.tsx
  • js/app/packages/block-md/component/TopBar.tsx
  • js/app/packages/block-pdf/component/TopBar.tsx
  • js/app/packages/block-project/component/TopBar.tsx
  • js/app/packages/block-unknown/component/TopBar.tsx
  • js/app/packages/block-video/component/TopBar.tsx
  • js/app/packages/core/component/ForwardToChannel.tsx
  • js/app/packages/core/component/RecipientSelector.tsx
  • js/app/packages/core/component/TopBar/ShareButton.tsx
  • js/app/packages/core/directive/focusInput.ts
📝 Walkthrough

Walkthrough

This pull request refactors and enhances mobile UI handling across several components. Changes include reworking focus input scrolling logic in MobileDrawer, adding mobile-specific layouts for channel forwarding and sharing features, adjusting switch styling for touch devices, and standardizing prop naming across shared components used in mobile experiences.

Changes

Cohort / File(s) Summary
Input focus handling
js/app/packages/app/component/mobile/MobileDrawer.tsx
Refactors focus input scrolling to use isEditableInput() utility instead of instanceof checks, simplifies event typing to accept FocusEvent directly, and adds a captured guard to prevent overlapping delayed scroll operations.
Mobile forwarding UI
js/app/packages/core/component/ForwardToChannel.tsx, js/app/packages/core/component/RecipientSelector.tsx
Introduces mobile-specific layout for channel forwarding via conditional rendering, extracts MobileForwardToChannelLayout component with recipient input and markdown editor, and standardizes the mobileHorizontalScroll prop to horizontalScroll for cross-device chip container scrolling.
Mobile sharing UI
js/app/packages/core/component/TopBar/ShareButton.tsx
Adds MobileShareDrawer component with tab-based interface ("Share", "People", "Link") for mobile, conditionally renders desktop Dialog or mobile drawer based on device type, and implements mobile input focusing behavior for recipient selection.
Touch-responsive styling
js/app/packages/core/component/FormControls/MiniToggleSwitch.tsx
Enhances toggle switch appearance on touch devices by increasing track and thumb dimensions using Tailwind touch:* utilities and adjusting thumb translation for larger touch layouts.

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive No pull request description was provided by the author, making it impossible to assess relevance or detail about the changeset. Add a pull request description explaining the mobile UI improvements, including details about the affected components and the rationale for these changes.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title follows conventional commits format with 'feat[mobile]:' prefix and is under 72 characters, but does not clearly describe the main changes across multiple components beyond just the share menu.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
js/app/packages/app/component/mobile/MobileDrawer.tsx (1)

19-35: ⚠️ Potential issue | 🟠 Major

Module-level captured flag creates a cross-drawer race and drops legitimate scrolls.

captured is declared at module scope, so it is shared across every call site of scrollToFocusedInput (this drawer, mobile-filter-drawer.tsx, and any future consumer). Two real problems follow:

  1. Rapid focus changes are silently dropped. If the user focuses input A and then focuses input B within the 300 ms window (e.g. keyboard Tab, programmatic focus, autofill), B's focus event early-returns because captured === true, and the scheduled scroll runs for A — whose focus may no longer be current.
  2. Cross-instance interference. If a share drawer and the filter drawer are both mounted, focusing in one while the other's timer is pending swallows the scroll in the second.

Additionally, if anything throws between captured = true and the setTimeout callback running, captured never resets and the utility is permanently disabled for the tab.

Prefer tracking the latest event and using clearTimeout, or scope state per container via a WeakMap<HTMLElement, number>.

🛠️ Proposed fix
-let captured = false;
-export function scrollToFocusedInput(e: FocusEvent, offset = 40) {
-  if (!isEditableInput(e.target as Element) || captured) return;
-  const input = e.target as HTMLElement;
-  const container = e.currentTarget as HTMLElement;
-  captured = true;
-  // Has to be delayed until after browser's native keyboard-show scroll completes
-  setTimeout(() => {
-    const inputRect = input.getBoundingClientRect();
-    const containerRect = container.getBoundingClientRect();
-    container.scrollTo({
-      top: container.scrollTop + (inputRect.top - containerRect.top) - offset,
-      behavior: 'smooth',
-    });
-    captured = false;
-  }, 300);
-}
+const pending = new WeakMap<HTMLElement, { timer: number; input: HTMLElement }>();
+export function scrollToFocusedInput(e: FocusEvent, offset = 40) {
+  if (!isEditableInput(e.target as Element)) return;
+  const input = e.target as HTMLElement;
+  const container = e.currentTarget as HTMLElement;
+  const existing = pending.get(container);
+  if (existing) clearTimeout(existing.timer);
+  // Has to be delayed until after browser's native keyboard-show scroll completes
+  const timer = window.setTimeout(() => {
+    pending.delete(container);
+    const latest = pending.get(container)?.input ?? input;
+    const inputRect = latest.getBoundingClientRect();
+    const containerRect = container.getBoundingClientRect();
+    container.scrollTo({
+      top: container.scrollTop + (inputRect.top - containerRect.top) - offset,
+      behavior: 'smooth',
+    });
+  }, 300);
+  pending.set(container, { timer, input });
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@js/app/packages/app/component/mobile/MobileDrawer.tsx` around lines 19 - 35,
The module-level captured flag in scrollToFocusedInput causes
dropped/nonspecific scrolls and cross-instance interference; replace it with
per-container timer tracking (e.g., a WeakMap<HTMLElement, number> or Map to
store timeout IDs keyed by the container element) so each container manages its
own pending timeout, clear any existing timeout with clearTimeout before
scheduling a new setTimeout, and ensure the timeout ID is removed from the map
in the callback (and in any error path) so state is not permanently stuck;
reference the existing scrollToFocusedInput function, the captured variable, and
the setTimeout/clearTimeout usage when implementing this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@js/app/packages/app/component/mobile/MobileDrawer.tsx`:
- Around line 53-60: The onFocusOut handler on Drawer.Content currently calls
setInputFocused(false) unconditionally causing a flicker when focus moves
between inputs; update the onFocusOut implementation (the Drawer.Content handler
that complements onFocusIn and uses setInputFocused and isEditableInput) to only
clear the signal when the focus is leaving to a non-editable element by
inspecting the FocusEvent.relatedTarget (or alternatively defer clearing via a
microtask and cancel if a subsequent onFocusIn occurs); ensure you reference the
same helpers (isEditableInput, scrollToFocusedInput) and keep the onFocusIn
logic intact so transitions between inputs inside the drawer do not transiently
set inputFocused to false.

In `@js/app/packages/core/component/ForwardToChannel.tsx`:
- Around line 67-78: The mobile recipient chip list is falling back to wrapping
because the horizontalScroll prop was left commented out on the
RecipientSelector component; enable horizontal scrolling by un-commenting and
passing horizontalScroll to RecipientSelector (the component instance in
ForwardToChannel.tsx) so the selector uses the horizontal chip scrollbar
behavior for mobile; ensure any related CSS/class props (e.g., class="border-1
border-edge-muted/50 p-1") remain compatible with horizontalScroll.
- Around line 80-123: The checkbox proxy lacks a visible focus state and is
duplicated; extract the UI into a single SendAsGroupToggle component (used by
ForwardToChannel) that uses props.canSendAsGroup(), props.sendAsGroupMessage(),
and props.setSendAsGroupMessage() and renders the same structure with CheckIcon,
then add focus-visible styling via the existing hidden input's peer classes
(e.g., use peer and peer-focus-visible:... or peer-focus:... on the proxy div to
show an outline/ring when keyboard-focused) so keyboard users see focus, and
replace both duplicated blocks with this new SendAsGroupToggle.

In `@js/app/packages/core/component/RecipientSelector.tsx`:
- Around line 495-496: The long template string building the class prop for the
chips should be split using the imported cn helper for readability: replace the
inline ternary in the class attribute on the element that uses
ref={props.horizontalScroll ? setChipsScrollRef : undefined} with a cn(...) call
that concisely lists shared classes and conditionally applies the horizontal
layout classes when props.horizontalScroll is true and the vertical/wrapped
layout classes otherwise; keep the ref logic referencing setChipsScrollRef
unchanged and ensure the two mode groups mirror the original class sets (shared:
"flex gap-1.5 text-ink scrollbar-hidden", horizontal: "flex-nowrap
overflow-x-auto sm:flex-wrap sm:overflow-x-hidden sm:max-h-[150px]
sm:overflow-y-auto pb-[2px] sm:pb-0", vertical: "flex-wrap max-h-[150px]
overflow-y-auto").

In `@js/app/packages/core/component/TopBar/ShareButton.tsx`:
- Around line 385-529: The mobile "People" and "Link" sections duplicate desktop
markup; extract a RecipientRow component and a PublicLinkSection component and
reuse them in both desktop and mobile layouts. Implement RecipientRow to render
the recipient entry (use props: recipient, channelNameMap, navigateToChannel,
removeChannelAccess, setChannelPermissions, and include the
Switch/Match/DmRecipientIcon/WideUsers, GroupChannelLabel and ShareOptions usage
shown), and implement PublicLinkSection to render the public-link area (use
props: publicAccessLevel, setPublicPermissions, copyPublicLink and include the
MiniToggleSwitch, ShareOptions and Copy button logic). Update the mobile and
desktop consumers to import and render RecipientRow for each recipient and
PublicLinkSection where the public link block appears, leaving layout containers
(ClippedPanel vs div) unchanged and preserving existing prop names like
MobileShareDrawerProps, ShareOptions, GroupChannelLabel, MiniToggleSwitch,
copyPublicLink, setPublicPermissions, navigateToChannel, removeChannelAccess,
setChannelPermissions.
- Around line 299-302: The createEffect currently derives state by reading
mobileTabs() and calling setActiveTab('share') when the active tab disappears;
replace this with a createMemo that computes an "effective" active tab from
mobileTabs() and activeTab() (do not call setActiveTab inside it) and have
consumers use that memoized value instead of activeTab(); keep the original
activeTab signal as the user's chosen tab, expose e.g. effectiveActiveTab =
createMemo(() => { ... }) which returns activeTab() when still present or the
fallback 'share' when not, and update all usages to read effectiveActiveTab
rather than relying on the effect-driven setActiveTab.
- Around line 316-322: Remove the problematic initialFocusEl usage that calls
document.querySelector synchronously during render: delete the initialFocusEl
prop branch around the MobileDrawer/Portal usage (the code referencing
initialFocusEl and the selector '[data-share-drawer-recipient] input'), or
replace it by scoping focus lookup via a ref passed into MobileDrawer if you
intend to keep it; rely on the existing ShareTrigger focusInput directive for
mobile focus. Also avoid relying on the global data-share-drawer-recipient
selector — if you must query, scope it to the drawer's root element (provided by
a ref) to disambiguate the two ForwardToChannel renderings. Ensure references to
initialFocusEl, MobileDrawer.Portal, ShareTrigger, focusInput, and
ForwardToChannel are updated accordingly.

---

Outside diff comments:
In `@js/app/packages/app/component/mobile/MobileDrawer.tsx`:
- Around line 19-35: The module-level captured flag in scrollToFocusedInput
causes dropped/nonspecific scrolls and cross-instance interference; replace it
with per-container timer tracking (e.g., a WeakMap<HTMLElement, number> or Map
to store timeout IDs keyed by the container element) so each container manages
its own pending timeout, clear any existing timeout with clearTimeout before
scheduling a new setTimeout, and ensure the timeout ID is removed from the map
in the callback (and in any error path) so state is not permanently stuck;
reference the existing scrollToFocusedInput function, the captured variable, and
the setTimeout/clearTimeout usage when implementing this change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0969f2f4-32fb-40bd-9fa6-ea962f39edb9

📥 Commits

Reviewing files that changed from the base of the PR and between 3489050 and 9159034.

📒 Files selected for processing (5)
  • js/app/packages/app/component/mobile/MobileDrawer.tsx
  • js/app/packages/core/component/FormControls/MiniToggleSwitch.tsx
  • js/app/packages/core/component/ForwardToChannel.tsx
  • js/app/packages/core/component/RecipientSelector.tsx
  • js/app/packages/core/component/TopBar/ShareButton.tsx

Comment thread js/app/packages/app/component/mobile/MobileDrawer.tsx
Comment thread js/app/packages/core/component/ForwardToChannel.tsx
Comment thread js/app/packages/core/component/ForwardToChannel.tsx
Comment thread js/app/packages/core/component/RecipientSelector.tsx Outdated
Comment thread js/app/packages/core/component/TopBar/ShareButton.tsx Outdated
Comment thread js/app/packages/core/component/TopBar/ShareButton.tsx Outdated
Comment thread js/app/packages/core/component/TopBar/ShareButton.tsx Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant