Add native modal Sheet primitive (Mob.UI.sheet/2) - #86
Open
GenericJam wants to merge 2 commits into
Open
Conversation
New :sheet node type composing ordinary Mob nodes as content — iOS .sheet presentation, Android Material 3 ModalBottomSheet (mob_new, companion PR). Composed children, medium/large detents, exactly-once native dismissal, native background/top-radius/scrim/drag-indicator styling with per-platform overrides, strict color/dimension validation (explicit nil/bool rejection prevents the iOS NSNull.longLongValue crash class). Presentation state lives in view/composable identity (mirrors MobToggle/MobSlider), not a boolean prop — a rerender that still includes the sheet updates content without re-presenting; removing the node dismisses it. Documented iOS limitation: system-owned sheet scrim opacity is not configurable via public APIs; Android applies the requested scrim exactly. Device-verified on the iOS simulator: presentation, custom drag indicator, background/corner radius, exactly-once dismissal via outside-tap, and reopen-after-dismiss cycling. MOB-101 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
4 tasks
- Fix moduledoc: on_dismiss delivers {:dismiss, tag}, not {:tap, tag}
- Extend drag-indicator completeness validation into ios:/android:
overrides (checked post-merge, not just on base props) — a partial
override now fails validation instead of silently rendering the
system default indicator
- Warn (not silently pass through) when a color prop resolves to
neither a theme token nor the base palette
- iOS: apply node.paddingEdgeInsets to sheet content (was dropped)
- iOS: give the sheet its own corner-radius sentinel (sheetCornerRadius,
-1 = unset) instead of sharing node.cornerRadius, so corner_radius: 0
is no longer indistinguishable from "not set"
- iOS: stop reporting a 0x0 frame for a sheet's id — the switch-case
view is an invisible anchor, not the sheet's real presented content
- Dedupe the atom-token guard shared by validate_color!/validate_radius!
into a single defguardp
- Add a regression test asserting Mob.UI.sheet/2's color props stay a
subset of Mob.Renderer's color-resolved prop set
Reviewed but left unchanged: the platform-override dispatch loop
(already delegates to validate_style_value!, not a reimplementation).
Not merged/released — awaiting review per standing instruction.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.
Summary
Mob.UI.sheet/2— a native modal bottom sheet (iOS.sheet, Android Material 3ModalBottomSheet, companionmob_newPR) that composes ordinary Mob nodes as content.[:medium, :large]detents,on_dismissfired exactly once via the existing tap-registry mechanism, native background/top-corner-radius/scrim/drag-indicator styling withios:/android:per-platform overrides (reused the existing generic platform-block flattening — no renderer changes needed there beyond adding the new color props).[:medium, :large]; colors accept a theme-token atom or0x00000000..0xFFFFFFFFARGB int, explicitly rejectingnil/true/false(the exact shape that previously crashed native code trying to read a color —NSNull.longLongValueon iOS); custom drag indicator requires all four geometry props together, width/height must be positive, rail height ≥ indicator height.MobSheetViewseeds@State private var isPresented = trueonce, the same mechanismMobToggle/MobSlideralready use to preserve user-driven state against a BEAM-pushed node rebuilt fresh every render. A rerender that still includes the sheet node updates content without dismissing/re-presenting; removing the node from the tree tears the whole presentation down. Full rationale indecisions/2026-08-26-native-sheet-primitive.md..sheetpresentation owns its dimming layer with no public API to configure opacity — Android applies the requested:scrimexactly, iOS uses system-black dimming at a fixed alpha. No private view-hierarchy manipulation.Test plan
mix test— 1109 passed, including newdescribe "sheet/2 ..."blocks intest/mob/ui_test.exs(constructor, detents, on_dismiss, style props incl. exhaustive invalid-color/indicator-geometry cases) anddescribe "sheet serialization"intest/mob/renderer_test.exs(token resolution, platform override flattening, on_dismiss handle registration, children recursion)mix format/mix credo --strict/mix erlfmt --check src//xcrun clang-format/swiftlint— all cleandismissedcounter increments by exactly 1 across two full open/dismiss cycles), and reopen-after-dismissLinear: MOB-101 (cross-repo — see the companion
mob_newPR for the Android generator side)