Skip to content

feat(remix)!: let a real button trigger RemixMenu - #337

Open
leoafarias wants to merge 2 commits into
feat/naked-ui-menu-open-on-tapfrom
feat/remix-menu-control-trigger
Open

leoafarias wants to merge 2 commits into
feat/naked-ui-menu-open-on-tapfrom
feat/remix-menu-control-trigger

Conversation

@leoafarias

Copy link
Copy Markdown
Collaborator

Description

Stacked on #335 (feat/naked-ui-menu-open-on-tap, adds NakedMenu.openOnTap); review and merge that first, then retarget this to main.

Adds RemixMenuTrigger.control so a real button, for example a preset's styled RemixButton, can be the menu trigger. Until now NakedMenu always wrapped the trigger in its own NakedButton, so a button returned from RemixMenuTrigger.builder produced two focus stops and two button nodes.

trigger: RemixMenuTrigger.control(
  builder: (context, menu) => RemixButton(
    label: 'Actions',
    focusNode: menu.focusNode,
    onPressed: menu.toggle,
  ),
),
  • The builder receives a RemixMenuTriggerHandle with isOpen, controller, focusNode, and toggle().
  • RemixMenu passes openOnTap: false and the effective focus node to NakedMenu, creating and disposing a node only when the caller supplies no triggerFocusNode. focusNode is the node that receives focus when the menu closes; on open, focus moves into the menu panel.
  • NakedMenu merges the control's semantics and the expanded state into one node, so a screen reader sees one expandable button. Do not wrap the control in your own MergeSemantics, and return a single control from the builder: everything it returns is merged into one node, so a split button or a loading button would merge its labels and actions.
  • Overlay, Escape, outside taps, focus return, the controller, and onOpen/onClose/onSelected are unchanged.
  • RemixMenuTrigger.label becomes String? because a control trigger has no label; the unnamed and .builder constructors still require one.
  • MenuStyler.trigger and RemixMenu.semanticLabel do not apply to a control trigger; a semanticLabel asserts.
  • Presets forward trigger untouched, so there is no codegen and no preset change.

Release: the remix beta that ships this needs naked_ui 1.3.0 published first; the naked_ui floor bump belongs to the naked_ui release PR.

Validation

  • packages/remix full suite (2850), the naked_ui menu, menubar, and semantics suites, dart analyze (remix, registry_source), root dart format, melos run ci:checks, melos run docs:check.
  • New tests: builder output rendered instead of the default trigger; pointer tap and Enter open and a second press closes; callbacks; one tab stop; Escape and item selection return focus (menu-owned and caller-supplied node); one semantics node with the control's label, button role, and expanded false to true (RemixButton and a Material TextButton that forms its own semantics node); a disabled control does not open and cannot take focus; owned node disposed and a supplied one left alone; semanticLabel asserts. Each fails when its source line is reverted or mutated.

Related Issues

Depends on #335.


Checklist

  • My PR includes unit or integration tests for all changed/updated/fixed behaviors.
  • I have updated or added relevant documentation (doc comments with ///).
  • I am prepared to follow up on review comments in a timely manner.

Breaking Change

  • Yes, this is a breaking change.
  • No, this is not a breaking change.

RemixMenuTrigger.label is now String?. Code that reads trigger.label and needs a non-null value must handle null (listed under Breaking changes in the CHANGELOG).

Add RemixMenuTrigger.control, whose builder receives a
RemixMenuTriggerHandle (isOpen, controller, focusNode, toggle()) so a
styled button such as RemixButton can be the menu trigger. RemixMenu
passes openOnTap: false to NakedMenu, owns a FocusNode for the control
when the caller supplies none, and merges the expanded state onto the
control's semantics. Overlay, Escape, outside taps, focus return, the
controller, and onOpen/onClose/onSelected are unchanged.

RemixMenuTrigger.label becomes String? because a control trigger has no
label; the unnamed and .builder constructors still require one.
MenuStyler.trigger and RemixMenu.semanticLabel do not apply to a control
trigger, and a semanticLabel asserts.

Also pin RemixPopover(openOnTap: false) with a button child: the wrapper
Focus is an extra tab stop and takes focus back when the popover closes.
NakedMenu now merges the openOnTap: false control's semantics and the
expanded state into one node itself, so RemixMenu no longer wraps the menu
in MergeSemantics; the outer wrapper double-merged. Document that a control
trigger's builder must return a single control, correct the focusNode docs
(it receives focus when the menu closes; on open focus moves into the menu
panel), and say the control field holds the builder.

Replace the hollow keyboard half of the disabled-control test with a check
that the control cannot take focus, and make the caller-supplied-node tests
use the supplied node instead of an always-true identity check.

Drop the RemixPopover(openOnTap: false) pin tests: a separate change fixes
that naked_ui bug and asserts the correct behavior.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-27T02:23:40.589771Z c5f6832 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

This branch has not been deployed

No deployments
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