fix(support): keep the Crisp composer above the iOS keyboard - #2615
Open
abalinda wants to merge 2 commits into
Open
fix(support): keep the Crisp composer above the iOS keyboard#2615abalinda wants to merge 2 commits into
abalinda wants to merge 2 commits into
Conversation
iOS never resizes the layout viewport for the software keyboard, so the drawer's `position: fixed; bottom: 0` kept pointing at the bottom of a window the keyboard was sitting on top of — and Crisp renders its composer on exactly that edge. Users could open support chat but not see what they were typing. `100vh`/`100dvh` are no help: they track browser chrome, not the keyboard, so CSS alone cannot see this. Measure `window.visualViewport` instead — the only thing that knows how much screen the keyboard ate — and lift the panel by that inset while clamping its height to what is still visible, so the conversation is pushed down rather than off the top edge. Two adjacent defects fall out of the same geometry and go with it: the panel's `max-h-[85vh]` clipped its own `h-[80vh]` iframe on shorter phones (the reported overflow), and `bottom: 0` with no safe-area padding put the composer under the home indicator on notched iPhones. Capacitor is unaffected — native support opens the Crisp SDK, not this iframe. TASK-20947
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📝 WalkthroughWalkthroughChangesSupport drawer keyboard handling
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant visualViewport
participant useVisualViewport
participant SupportDrawer
visualViewport->>useVisualViewport: Emit resize or scroll event
useVisualViewport->>SupportDrawer: Return viewport height and keyboard inset
SupportDrawer->>SupportDrawer: Apply bottom offset and constrained height
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Contributor
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
Contributor
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/hooks/useVisualViewport.ts`:
- Around line 31-33: Reset the hook’s measurement state when useVisualViewport’s
enabled flag is false, before returning from the effect, so keyboardInset and
visibleHeight cannot persist across drawer closes. Preserve the existing
viewport setup and measurement behavior when enabled, and add a regression test
covering close, viewport restoration, and reopen.
🪄 Autofix
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: CHILL
Plan: Pro
Run ID: 53650c07-70d4-4176-98c3-bf6d4bd5ac6e
📒 Files selected for processing (4)
src/components/Global/SupportDrawer/__tests__/SupportDrawer.test.tsxsrc/components/Global/SupportDrawer/index.tsxsrc/hooks/__tests__/useVisualViewport.test.tssrc/hooks/useVisualViewport.ts
Contributor
Code-analysis diffPainscore total: 6346.7 → 6353.41 (+6.71) 🆕 New findings (10)
✅ Resolved (6)
📈 Painscore deltas (top movers)
|
Reset-on-close is the serious one. `translateY(100%)` resolves against the element's OWN height, so once the keyboard shrank the panel to ~460px it no longer travelled far enough to clear a 340px lift — closing the drawer with the keyboard still up left an opaque sheet over the bottom third of the app. The hook unsubscribes on close, so it could never see the keyboard leave and correct itself. Drop the measurement instead of freezing it. Also: - Stand down while pinch-zoomed. Zoom shrinks the visual viewport exactly like a keyboard does; a zoomed reader isn't typing, and adjusting for them just makes the panel jump. `maximum-scale=1` rules out iOS focus auto-zoom, so scale > 1 is always deliberate. - Lower the noise floor 80px → 40px. The old floor cleared scrollbar and sub-pixel noise but also swallowed the ~45px accessory bar iOS shows for a hardware keyboard — which hides a composer just as well. - Reserve a strip above the panel. The clamp made the panel exactly fill the visible viewport, putting the drag handle under the notch and leaving no backdrop to tap. Slack when no keyboard is up, so the resting look is unchanged. Tests restore window.innerHeight/visualViewport so later blocks in the file don't inherit a fake viewport.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes TASK-20947 — Crisp session · Discord report
The bug
Open support chat on iOS, tap the message field, and the composer disappears behind the keyboard. You can type, you just can't see what you typed.
Why
iOS never resizes the layout viewport for the software keyboard — it shrinks only the visual viewport and scrolls.
SupportDrawer's panel isposition: fixed; bottom: 0, andbottom: 0means the bottom of the layout viewport: the edge the keyboard is sitting on top of. Crisp renders its composer on exactly that edge.100vhand100dvhare no help —dvhtracks browser chrome (the collapsing Safari toolbar), not the keyboard. CSS cannot see the keyboard at all.window.visualViewportis the only thing that can.The fix
New
useVisualViewport(enabled)hook returns the visible height and how many px of the layout viewport's bottom edge are hidden:The
offsetTopterm matters: iOS scrolls the focused field into view, and the panel is pinned to the layout viewport, so that scroll hides it too. Values under a 40px floor are discarded as sub-pixel/scrollbar noise — without it, a 0.8px rounding delta reads as "keyboard up" on every device. The floor sits below the ~45px accessory bar iOS shows for a hardware keyboard, which hides a composer just as well as the full keyboard.The panel then lifts by that inset and clamps its height to what's still on screen, so the conversation is pushed down rather than off the top edge. The clamp reserves
env(safe-area-inset-top) + 24pxso the drag handle stays clear of the notch and a backdrop strip remains tappable; with no keyboard up that reserve is slack and85dvhwins, leaving the resting look unchanged.Zoom is explicitly not a keyboard: pinch shrinks the visual viewport identically, so
scale > 1stands the hook down entirely and CSS takes over.maximum-scale=1in the root viewport rules out iOS focus auto-zoom, so a zoom is always deliberate.Two adjacent defects, same geometry
Both are in the ticket's scope and fall out of the same lines:
max-h-[85vh]wrapping a fixedh-[80vh]iframe plus a ~38px drag handle. Below ~760px of viewport the max-height clipped its own content. Height is now explicit and the iframe row isflex-1 min-h-0, so it can't disagree with its parent.bottom: 0with no padding put the composer under the home indicator on notched iPhones. NowpaddingBottom: env(safe-area-inset-bottom)— dropped while the keyboard is up, since the keyboard already covers the home indicator and padding there would wedge a dead strip between composer and keys.Scope
isCapacitor()opens the native Crisp SDK, never this drawer's iframe (SupportDrawer/index.tsx:56-92). Its keyboard is already handled byKeyboardResize.NativeinuseNativePlugins.ts.SupportDraweris mounted on every route, and these events fire on every keystroke-driven scroll. State updates bail on unchanged values so momentum scroll can't cause a render storm.Tests
src/hooks/__tests__/useVisualViewport.test.ts(10) — inset math, theoffsetTopterm, the noise floor, the hardware accessory bar, the zoom stand-down, reset-on-disable, unmount teardown, and browsers with novisualViewport.SupportDrawer.test.tsx(+2) — panel is flush atbottom: 0pxwith no keyboard, lifts tobottom: 340pxwhen the visual viewport drops to 460 of 800. Onlybottomis assertable: jsdom's CSS parser drops bothenv()andmin().Local gate
Prettier ✅ · typecheck ✅ ·
npm test179/179 suites, 2365 passed ✅ ·npm run build✅Manual QA needed
Device verification is the one thing this can't self-check — needs a real iPhone:
Note (not fixed here)
crisp-proxy/page.tsx:12says the proxy is embedded "from SupportDrawer and SupportPage" — there is no SupportPage;SupportDraweris the only consumer. Stale comment, left alone to keep this diff surgical.Code review round (
/code-review medium)Five findings, four applied in
5f4adfb:Reset-on-close (HIGH) — the one that mattered.
translateY(100%)resolves against the element's own height. Once the keyboard shrank the panel to ~460px it no longer travelled far enough to clear a 340px lift, so closing the drawer with the keyboard still up left an opaque sheet over the bottom third of the app. Worse, the hook unsubscribes on close, so it could never observe the keyboard leaving and self-correct. The measurement is now dropped rather than frozen.Pinch-zoom false positive (MED). Zoom shrinks the visual viewport indistinguishably from a keyboard;
scale > 1now stands the hook down.Panel filled the visible viewport exactly (MED). The lift and the clamp are algebraically complementary, so the panel's top landed precisely on the visible top edge — drag handle under the notch, no backdrop to tap. Hence the reserved strip.
Accessory-bar blind spot (LOW). Noise floor 80px → 40px.
Not applied — transitioning
bottom/height(LOW). Suggested to smooth the ~250ms keyboard animation, currently a one-frame snap. Declined: both properties trigger layout, so a transition forces the Crisp iframe to reflow every frame for the duration instead of once. This component already carries scars from WKWebView content-process crashes under memory pressure (see the eager-mount comment at the top ofSupportDrawer/index.tsx) — one reflow beats fifteen.Screenshots —⚠️ NONE, and this one genuinely can't have them
Not an oversight and not laziness: the states this PR changes cannot be rendered by a headless browser. They require a real iOS software keyboard shrinking the visual viewport, which desktop Chromium has no equivalent of. A 375×667 screenshot would show the resting drawer — which this PR deliberately leaves pixel-identical (with no keyboard up, the
85dvhterm wins every clamp).Faking it by stubbing
window.visualViewportin a page init script would produce a picture of the stub, not of iOS, and would invite more confidence than it earns.What stands in for visual evidence instead: the geometry is asserted numerically.
SupportDrawer.test.tsxpins the panel atbottom: 0pxat rest andbottom: 340pxwhen the visual viewport drops to 460 of 800 — the exact number a screenshot would be checked against by eye.The manual QA checklist above is therefore load-bearing, not optional. Please run it on a real device before approving.
Design notes / accepted trade-offs
Declined: transitioning
bottom/heightalongsidetransform./code-reviewsuggested it to smooth the ~250ms keyboard animation, which is currently a one-frame snap. Both properties trigger layout, so a transition makes the Crisp iframe reflow on every frame for the duration instead of exactly once. This component already carries scars from WKWebView content-process crashes under memory pressure — see the eager-mount comment at the top ofSupportDrawer/index.tsx. One reflow beats fifteen; the snap stays.Declined:
'use client'on the new hook (flaggednextjs-missing-use-clientby code-analysis). All three sibling Crisp hooks omit it — includinguseCrispTokenId, which isuseState/useEffectexactly like this one. Next propagates the client boundary through imports from the already-'use client'SupportDrawer, and the production build confirms it. Adding it here alone would be an inconsistent one-off.Smell verdict: adds none. The code-analysis
+6.71painscore is the new file existing at all (0 → 6.2); its six "resolved" entries are the same pre-existingSupportDrawerfindings re-reported at shifted line numbers, not fixes.