Skip to content

Add native modal Sheet primitive (Mob.UI.sheet/2) - #86

Open
GenericJam wants to merge 2 commits into
masterfrom
feat/mob-sheet-native-primitive
Open

Add native modal Sheet primitive (Mob.UI.sheet/2)#86
GenericJam wants to merge 2 commits into
masterfrom
feat/mob-sheet-native-primitive

Conversation

@GenericJam

Copy link
Copy Markdown
Owner

Summary

  • New Mob.UI.sheet/2 — a native modal bottom sheet (iOS .sheet, Android Material 3 ModalBottomSheet, companion mob_new PR) that composes ordinary Mob nodes as content.
  • Composed children, [:medium, :large] detents, on_dismiss fired exactly once via the existing tap-registry mechanism, native background/top-corner-radius/scrim/drag-indicator styling with ios:/android: per-platform overrides (reused the existing generic platform-block flattening — no renderer changes needed there beyond adding the new color props).
  • Strict validation: detents must be a nonempty duplicate-free subset of [:medium, :large]; colors accept a theme-token atom or 0x00000000..0xFFFFFFFF ARGB int, explicitly rejecting nil/true/false (the exact shape that previously crashed native code trying to read a color — NSNull.longLongValue on iOS); custom drag indicator requires all four geometry props together, width/height must be positive, rail height ≥ indicator height.
  • Presentation state lives in view identity, not a boolean propMobSheetView seeds @State private var isPresented = true once, the same mechanism MobToggle/MobSlider already 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 in decisions/2026-08-26-native-sheet-primitive.md.
  • Documented (not "fixed") limitation: iOS's .sheet presentation owns its dimming layer with no public API to configure opacity — Android applies the requested :scrim exactly, iOS uses system-black dimming at a fixed alpha. No private view-hierarchy manipulation.

Test plan

  • mix test — 1109 passed, including new describe "sheet/2 ..." blocks in test/mob/ui_test.exs (constructor, detents, on_dismiss, style props incl. exhaustive invalid-color/indicator-geometry cases) and describe "sheet serialization" in test/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 clean
  • Device-verified on the iOS simulator: presentation with custom background/corner-radius/drag-indicator, exactly-once dismissal via outside-tap (confirmed screen stays alive and dismissed counter increments by exactly 1 across two full open/dismiss cycles), and reopen-after-dismiss

Linear: MOB-101 (cross-repo — see the companion mob_new PR for the Android generator side)

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>
- 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant