Documentation for DropdownMenu, Container, and EditableInput from rn-markdown-editor/componentssteemCreated with Sketch.

in Steem Dev2 days ago

DropdownMenu

A Radix-style compound dropdown menu for React Native (Expo/web via NativeWind) — a Pressable trigger that opens a positioned popover Modal containing items, checkboxes, radios, labels, separators, and (structurally, if not yet fully wired) submenus.

import {
  DropdownMenu,
  DropdownMenuTrigger,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuCheckboxItem,
  DropdownMenuSeparator,
  DropdownMenuLabel,
} from "rn-markdown-editor/components";

<DropdownMenu>
  <DropdownMenuTrigger variant="outline">Options</DropdownMenuTrigger>
  <DropdownMenuContent>
    <DropdownMenuLabel>Actions</DropdownMenuLabel>
    <DropdownMenuItem onSelect={() => save()}>Save</DropdownMenuItem>
    <DropdownMenuCheckboxItem checked={wrap} onSelect={() => setWrap(!wrap)}>
      Word wrap
    </DropdownMenuCheckboxItem>
    <DropdownMenuSeparator />
    <DropdownMenuItem disabled onSelect={() => remove()}>Delete</DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>

How it works internally

Items are registered, not rendered as children. DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioItem, DropdownMenuLabel, and DropdownMenuSeparator are all "phantom" components — each one returns null and instead calls registerItem/unregisterItem on a shared NativeMenuContext inside a useEffect, pushing a plain data object ({ key, type, label, checked, disabled, onSelect, ... }) into a context-held items array. DropdownMenuContent is the only thing that actually renders JSX for them, mapping over items and switch-casing on type (separator → a divider View, label → styled Text, everything else → a shared HoverableItem). This means item order in the tree drives registration order, but visual rendering is fully centralized in one place.

Context is one object per menu instance. NativeDropdownMenu (the DropdownMenu export) owns open, triggerLayout, and items state and hands them down through NativeMenuContext, memoizing the context value so unrelated state changes don't re-render every consumer. useDropdownMenu() is exposed as an escape hatch to read this context directly.

Opening captures a real on-screen measurement. DropdownMenuTrigger doesn't just toggle a boolean — handlePress calls measureRef.current?.measure(...) on an absolutely-positioned invisible View overlaying the trigger, capturing pageX/pageY/width/height into triggerLayout before setting open to true, so the popover always has real coordinates to position against, even on the very first open.

Positioning is measure → flip → clamp, same as ColorPicker. DropdownMenuContent renders once off-screen-ish to get its own size via onLayout (dropdownHeight/dropdownWidth), then a useMemo computes top/left/right: it opens above the trigger instead of below when there isn't enough room below and there's more room above than below, and it switches from an explicit left to a right anchor when the popover would overflow the screen's right edge. This is the identical measure→flip→clamp strategy the ColorPicker panel uses on web, just driven by RN Dimensions.get("window") instead of measureInWindow.

One shell for everything: a transparent full-screen Modal. There's no platform branching for native vs. web presentation here — DropdownMenuContent always renders a transparent fade-Modal with a full-bleed Pressable backdrop (onPress={handleClose}) and an absolutely-positioned inner Pressable that calls stopPropagation so tapping the menu itself doesn't close it.

Trigger styling is variant × interaction-state, computed, not CSS. DropdownMenuTrigger derives bg/text/border for each of five ButtonVariants (primary/secondary/outline/ghost/link) in a useMemo, then picks between base/hover/pressed colors based on live isHovered/isPressed state — with per-instance override props (hoveredBackgroundColor, pressedTextColor, etc.) taking precedence over the variant defaults at each of the three states independently.

Render-prop trigger children get the computed state back. If children is a function, it's called with { hovered, pressed, textColor, backgroundColor } instead of receiving a plain ReactNode — so a custom trigger can restyle itself using the exact colors the built-in Text path would have used, without recomputing the variant logic.

Group, Sub, and SubContent are structural no-ops today. They render only <>{children}</> — present so the compound API surface matches a Radix-like shape (and so submenu JSX doesn't break), but they don't currently add nesting, positioning, or hover-to-open behavior of their own; only SubTrigger renders anything (a row with a trailing chevron).

Key props — DropdownMenuTrigger

PropTypeDefaultDescription
variant`"primary" \"secondary" \"outline" \"ghost" \"link"`"ghost"Visual style, drives base/hover/pressed color computation
size`"default" \"sm" \"lg" \"icon" \"none"`"default"Controls padding via a static TRIGGER_SIZE_PADDING map
children`ReactNode \(state) => ReactNode`Plain content, or a render prop receiving { hovered, pressed, textColor, backgroundColor }
textColor / hoveredTextColor / pressedTextColorstringPer-state text color overrides
backgroundColor / hoveredBackgroundColor / pressedBackgroundColorstringPer-state background overrides
borderColor / hoveredBorderColor / pressedBorderColorstringPer-state border overrides (only visible with variant="outline")
disabledbooleanDims to opacity: 0.5 and blocks onPress

Key props — DropdownMenuContent

PropTypeDescription
contentStyleViewStyleExtra style merged onto the positioned wrapper
sideOffsetnumberDeclared in the type but not yet read internally — positioning currently uses a fixed OFFSET = 4

Key props — item components

ComponentNotable props
DropdownMenuItemonSelect, onClick (both fire — kept for Radix-API compatibility), disabled, inset
DropdownMenuCheckboxItemchecked, onSelect, disabled
DropdownMenuRadioItemchecked, onSelect, disabled
DropdownMenuLabelinset, className
DropdownMenuShortcutRight-aligned, de-emphasized trailing text (e.g. a keybinding hint)

Container

A themed max-width, auto-centered wrapper View — the standard content-width constraint used to keep page/section content from stretching edge-to-edge on wide (mostly web) viewports.

import { Container } from "rn-markdown-editor/components";

<Container size="md">
  <Article />
</Container>

How it works internally

Size is a theme lookup with a per-instance override chain. size ("xs" | "sm" | "md" | "lg" | "xl" | "2xl") is resolved as config?.containerSizes?.[size] ?? container.containerSizes[size] — a component-level config override (from useComponentConfig("container")) wins if present, otherwise it falls back to the theme's base containerSizes map from useTheme(). Padding and border radius follow the identical override-then-fallback pattern.

Everything is centered and full-width by default. The computed style always sets width: "100%" and alignSelf: "center", so maxWidth is the only thing actually constraining layout — on a narrow screen the container simply fills it; on a wide one it caps out and centers itself with equal space on both sides.

Style computation is memoized against its actual dependencies. The useMemo for containerStyle lists out container.containerSizes, container.padding, container.borderRadius, config, size, and style individually (rather than depending on the whole theme object), so it only recomputes when something that could actually change the output has changed.

It's a thin, memoized pass-through. Container is wrapped in React.memo and spreads all remaining ViewProps onto the underlying View, so anything not explicitly destructured (onLayout, testID, accessibilityRole, etc.) passes straight through untouched.

Key props

PropTypeDefaultDescription
size`"xs" \"sm" \"md" \"lg" \"xl" \"2xl"`— (required)Named max-width, resolved against theme/config containerSizes
styleViewProps["style"]Merged after the computed size/padding/radius styles
childrenReactNodeContent rendered inside the constrained View
(all other ViewProps)Spread directly onto the underlying View

EditableInput

A click/double-tap-to-edit text field: renders as a plain, borderless label until the user double-taps (or double-clicks on web) it, at which point it swaps into a fully editable, styled Input, then reverts back to label mode on blur or submit.

import { EditableInput } from "rn-markdown-editor/components";

<EditableInput
  value={title}
  onChange={setTitle}
  onSubmit={(final) => renameDoc(final)}
  previewVariant="transparent"
  editVariant="outline"
/>

How it works internally

One boolean drives two completely different presentations. isEditing state switches three things on the underlying Input simultaneously: variant (previewVariant vs. editVariant, default "transparent" vs. "outline"), typeable, and readOnly — so "preview mode" and "edit mode" are really just one component reinterpreting the same value under different chrome, rather than two separate components being swapped.

Entering edit mode is manual-focus, not props-driven. enterEditMode sets isEditing(true) and then calls inputRef.current?.focus() inside a requestAnimationFrame, deliberately waiting a tick so the TextInput has already re-rendered as editable before the imperative focus call fires — calling focus() in the same tick as flipping readOnly off is unreliable on native.

Double-tap detection is hand-rolled for native, native for web. On native platforms, a transparent Pressable overlay (StyleSheet.absoluteFill, shown only while not editing) compares Date.now() against a lastTapRef timestamp against a DOUBLE_TAP_DELAY of 300ms to detect a double-tap manually. On web, this is augmented (not replaced) with a real onDoubleClick handler, since browsers already fire that event reliably — both paths call the same enterEditMode.

The overlay only exists while not editing. The double-tap-catching Pressable is conditionally rendered ({!isEditing && <Pressable .../>}) so that once the field becomes editable, there's no invisible layer sitting on top of the real TextInput intercepting touches meant for text selection/cursor placement.

Blur is the default commit point, but it's opt-out. handleBlur checks submitOnBlur (default true) before doing anything: if true, it exits edit mode and calls onSubmit(value); either way it still forwards the original onBlur event, so a consumer that sets submitOnBlur={false} can drive its own commit/cancel flow (e.g. explicit Save/Cancel buttons) while still observing blur.

Imperative API mirrors the internal transitions exactly. useImperativeHandle exposes edit/cancel/focus/bluredit is literally enterEditMode, and cancel calls exitEditMode then fires onCancel(value) with whatever the value was at the moment of cancellation, letting a parent trigger cancel-and-revert logic (e.g. on an Escape keypress caught elsewhere) without duplicating the exit logic.

Style objects are computed once per state, not inlined per render. computedTextStyle and computedContainerStyle are built by conditionally spreading in config values (fontSize, fontWeight, padding, borderRadius) and the current stateColors (config-driven colors keyed by "editing" vs. "preview") only when each is actually defined — avoiding undefined style keys — and are memoized against config/stateColors so they don't get rebuilt on every keystroke.

Key props

PropTypeDefaultDescription
valuestring— (required)Controlled text value
onChange(text: string) => void— (required)Fires on every keystroke while editing
onSubmit(value: string) => voidFires once editing ends (on blur, if submitOnBlur)
onCancel(value: string) => voidFires when editing is cancelled, with the value at cancel time
disabledbooleanfalseFully blocks entering edit mode
previewVariantInputVariant"transparent"Input variant used while not editing
editVariantInputVariant"outline"Input variant used while editing
submitOnBlurbooleantrueWhether blur automatically exits edit mode and calls onSubmit
containerStyleViewStyleStyle for the wrapping, relatively-positioned View

Imperative ref (EditableInputRef)

MethodDescription
edit()Programmatically enters edit mode and focuses the field
cancel()Exits edit mode and calls onCancel(value)
focus()Focuses the underlying TextInput (effective only while editing)
blur()Blurs the underlying TextInput
Sort:  

Welako

@surajadhikari

Hello surajadhikari! 📸 If you love taking photos, check out WelSnap – the photo app on the Steem blockchain. You share only real, live-captured moments, control exactly how visible your location is, and can also save pictures privately just for yourself. Every photo you upload gets a vote from me – I check in every day. And every uploaded photo automatically enters our monthly raffle: one photo is drawn from all submissions and wins 100 STEEM. Find more information about the app here: https://welako.app/@greece-lover/welsnap-the-photo-app-that-shares-real-moments-and-belongs-to-you-20260720164757 Curious? 👉 welako.app/snap


For several months now, I have been active as a Witness on Steem — currently ranked #42, with my own server and backup. My goal is to work together with others to improve the system so that the price rises sustainably and we all have a future on this blockchain. If you like my commitment, I would be happy to receive your Witness vote.

Vote with one click ↗


If you don't want to receive more comments like this, you can opt out here: https://welako.app/welpromo/optout?token=31196cac10b1a67c22df939405f3a2554dfeff4ef33fd5af007cacda26b1ccb0&u=surajadhikari

Not interested? Click the link above or simply reply STOP under this comment.

Note: If you don't reply to this comment, it will be automatically removed within a week.