Long-press pop menu
Press & hold a target to pop its actions
Hold ~0.5s (watch it dip), right-click, or focus it and press Enter / Menu / Shift+F10 (or its ⋯). In the menu: ↑ ↓ move, Enter runs, Esc closes.
Routing every session through the credential store for credentials. Ship it.
NockerlMenu opened 0 times · selected: none · pointer, right-click, and keyboard all work; the island is live.
Gestures
Section titled “Gestures”The pop is the same contextual action menu whichever gesture opens it. One menu, several triggers:
| Gesture | Where | Opens |
|---|---|---|
| Long-press (press-and-hold) | Android (primary), touch web | the pop menu |
| Hover → the ⋯ affordance, click | desktop / mouse web | the same pop menu (the ⋯ is the DESKTOP entry point) |
| Right-click / Ctrl-click | web, macOS (.contextMenu) |
the same pop menu |
| Keyboard (focus ⋯, or Shift+F10 → Enter) | web a11y | the same pop menu |
The hover ⋯ is therefore not an odd extra affordance; it is simply how a mouse / keyboard user reaches the long-press menu (long-press is a touch gesture, so the desktop needs a visible entry point to the same menu).
Tap-to-select is a different gesture. Tapping a chat bubble (its onSelect) selects the item (a toggle + soft cyan wash); it does not open the action menu. Reach for the pop menu when an element needs a menu of actions; reach for tap-to-select when it needs selection. The two compose: a bubble can be both selectable and long-press-poppable.
// Web: React, consuming @dizyx/nockerl-tokens. (Web is the laggard platform. Spec + live demo,// not yet exported from @dizyx/nockerl-react; promotion is tracked.)
export function MessageBubble({ message }: { message: Message }) { return ( <LongPressPop actions={[ { id: 'copy', label: 'Copy', icon: <CopyIcon />, onSelect: => copy(message) }, { id: 'select', label: 'Select', icon: <CheckIcon />, onSelect: => select(message) }, { id: 'share', label: 'Share', icon: <ShareIcon />, onSelect: => share(message) }, { id: 'delete', label: 'Delete', icon: <TrashIcon />, tone: 'danger', onSelect: => remove(message) }, ]} > <ChatBubble role="agent">{message.text}</ChatBubble> </LongPressPop> );}// Android (Jetpack Compose). PUBLISHED: the shared press-feedback modifier// com.dizyx.nockerl.design.components.longPressPop (com.dizyx.nockerl.design on GitHub// Packages Maven) dips the target + plays a haptic + an accent ripple from the touch// point, then opens an anchored DropdownMenu of actions.import com.dizyx.nockerl.design.components.longPressPop
var menuOpen by remember { mutableStateOf(false) }
Box { NockerlCard( modifier = Modifier.longPressPop( onLongPress = { menuOpen = true }, // pop fires the haptic + ripple, then this onTap = { /* primary action */ }, ), ) { MessageBody(message) }
DropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) { DropdownMenuItem(text = { Text("Copy") }, leadingIcon = { Icon(Icons.Default.ContentCopy, null) }, onClick = { copy(message); menuOpen = false }) DropdownMenuItem(text = { Text("Select") }, leadingIcon = { Icon(Icons.Default.Check, null) }, onClick = { select(message); menuOpen = false }) DropdownMenuItem(text = { Text("Share") }, leadingIcon = { Icon(Icons.Default.Share, null) }, onClick = { share(message); menuOpen = false }) HorizontalDivider() DropdownMenuItem( text = { Text("Delete", color = LocalNockerlColors.current.statusError) }, leadingIcon = { Icon(Icons.Default.Delete, null, tint = LocalNockerlColors.current.statusError) }, onClick = { delete(message); menuOpen = false }, ) }}// macOS: SwiftUI (canonical). NockerlVoice/UI/HistoryView.swift uses the system// .contextMenu, which lifts the row over the system dim and shows the actions.// role: .destructive renders Delete in the platform danger style.TranscriptionRow(record: record) .contextMenu { Button { copy(record) } label: { Label("Copy", systemImage: "doc.on.doc") } Button { select(record) } label: { Label("Select", systemImage: "checkmark") } Button { share(record) } label: { Label("Share", systemImage: "square.and.arrow.up") } Divider() Button(role: .destructive) { delete(record) } label: { Label("Delete", systemImage: "trash") } }Parameters
Section titled “Parameters”| Prop | Type | Default | Description |
|---|---|---|---|
children * | ReactNode | The content element the pop is triggered on. It keeps its own surface and lifts above the scrim when popped. | |
actions * | PopAction[] | Action rows: { id, label, icon, tone?, onSelect }. A tone: 'danger' row renders in the error token and is divider-separated at the end. | |
holdMs | number | 500 | Press-hold threshold before the menu pops. A live scale dip tracks the timer. |
disabled | boolean | false | When set, no gesture is detected and no feedback plays (the target stays interactive for its own tap). |
onOpenChange | (open: boolean) => void | Notified when the menu opens / closes (e.g. to pause a list, fire a haptic). |
Modifier.longPressPop is the shared press-feedback primitive (published in com.dizyx.nockerl.design.components); it owns the dip + ripple + haptic and calls onLongPress. The anchored list is a standard DropdownMenu opened from that callback.
| Parameter | Type | Default | Description |
|---|---|---|---|
onLongPress * | () -> Unit | Invoked once the long-press is recognized, firing the pop (ripple + dip) and whatever the caller opens (e.g. the DropdownMenu). | |
enabled | Boolean | true | When false, no gestures are detected and no feedback plays. |
onTap | (() -> Unit)? | null | Invoked on a normal tap (the element's primary action); the press-down still drives the scale dip. |
rippleColor | Color? | null | Tint of the radial bloom from the touch point. Defaults to accentPrimary (the brand cyan). |
haptic | Boolean | true | Fire a HapticFeedbackType.LongPress pulse when the long-press is recognized. |
SwiftUI exposes the pattern as the system .contextMenu { … } modifier on a view. There are no explicit parameters: it takes a @ViewBuilder of Buttons; the system supplies the lift, the dim, and the long-press / right-click / Control-click triggers.
| Style | Type | Default | Description |
|---|---|---|---|
.contextMenu | ViewModifier | Attaches the contextual menu. The system lifts the view over a dim and reveals the actions on long-press / right-click / Control-click. | |
Button { … } | View | One action row. Label(title, systemImage:) gives it an icon + text. | |
role: .destructive | ButtonRole | Renders the action (Delete) in the platform danger style. | |
Divider() | View | Separates the destructive action from the rest, matching the other platforms. |