Skip to content

feat(mix): rework Pressable input and semantics, add focus-visible variant - #1005

Closed
leoafarias wants to merge 1 commit into
mainfrom
feat/pressable-focus-visible-semantics
Closed

feat(mix): rework Pressable input and semantics, add focus-visible variant#1005
leoafarias wants to merge 1 commit into
mainfrom
feat/pressable-focus-visible-semantics

Conversation

@leoafarias

Copy link
Copy Markdown
Member

Summary

Interaction-subsystem changes to core mix extracted from the Tailwind parity branch (leoafarias/mix-tailwind-compat). They were found during parity work, but none of them are Tailwind-specific — they are standalone framework fixes plus two general-purpose features, so they ship as their own PR. The parity branch stacks on top of this one.

Bug fixes (real bugs, reproducible without mix_tailwinds)

  • Nested widget-state discovery (style.dart): Style.widgetStates only scanned top-level WidgetStateVariants. A style whose only widget-state variants were nested (onDark(BoxStyler().onHovered(...)), onBreakpoint(...)) or negated (onNot(hover)) reported no dependencies, so StyleBuilder never attached a MixInteractionDetector and the variant never activated. Now recursive with identity-based cycle protection, and driven by the new ContextVariant.widgetStateDependencies so non-WidgetStateVariant variants (Not, FocusVisible) participate.
  • Duplicate pressed-state owners (pressable_widget.dart): both GestureDetector (tapDown/tapUp/tapCancel) and MixInteractionDetector's Listener wrote pressed to the same controller, with conflicting timing (e.g. tapCancel during a long-press cleared pressed while the pointer was still down). The Listener is now the single owner.
  • Controller lifecycle: late final _controller ignored widget.controller swaps, and dispose() consulted the current widget to decide ownership — swapping external↔internal could dispose a controller the widget didn't own or leak the one it did. Fixed via _ownsController + didUpdateWidget.
  • Semantics correctness: the semantics node exposed onTap even when disabled, had no enabled state or longPress action, and GestureDetector's implicit semantics duplicated actions. Actions are now gated on enabled, enabled is exposed, and the GestureDetector is always excluded from semantics.
  • PressableBox.enableFeedback existed but was never forwarded to Pressable; the full surface (cursor, focus, keyboard, controller, actions, semantics) is now forwarded.
  • Keyboard activation previously bound ActivateIntentonPress with no enabled check and no pressed feedback. Now Space/Enter/numpad Enter model held state: pressed on key down, activate once on key up, repeats suppressed, and cancellation on focus loss, disable, controller swap, and dispose.

New features

  • FocusVisibleVariant / ContextVariant.focusVisible() / onFocusVisible(...): CSS :focus-visible equivalent, keyed off FocusManager.highlightMode (the same input-modality signal FocusableActionDetector uses), made reactive inside Pressable via the new internal FocusHighlightModeProvider.
  • PressableSemanticsRole (button / link / none) + semanticsLabel.

mix_protocol's schema inventory registers the new public symbols as v1 out-of-scope.

Breaking changes

  • semanticButtonLabelsemanticsLabel (no remaining usages in-repo).
  • Deprecated onKey removed; use onKeyEvent (it runs first and can override activation).
  • Pressable reserves Space/Enter/numpad Enter: custom actions no longer receive ActivateIntent for those keys. Deliberate contract so held-key state stays consistent; documented in the CHANGELOG.

Note one judgment call: activation fires on key-up for both Space and Enter (the web fires Enter on key-down). The uniform key-up model was chosen so the held/pressed lifecycle is consistent across keys.

Test plan

  • packages/mix: full suite passes (2,852 tests), including new coverage: keyboard hold/activate-once/cancel paths, controller-swap state transfer, onKeyEvent precedence, reserved-keys-vs-custom-actions, focus-visible modality switching, semantics role/action matrix, and nested/negated/cyclic variant discovery.
  • packages/mix_protocol: inventory + contract tests pass.
  • dart analyze packages/mix packages/mix_protocol: no issues.

…riant

New features:
- FocusVisibleVariant, ContextVariant.focusVisible(), and onFocusVisible(),
  driven by FocusManager.highlightMode through FocusHighlightModeProvider
  (the same modality signal FocusableActionDetector uses)
- ContextVariant.widgetStateDependencies so any context variant can declare
  the widget states it needs tracked
- PressableSemanticsRole (button/link/none) and semanticsLabel

Fixes:
- Style.widgetStates now discovers state requirements recursively through
  nested and negated variants (previously a style whose only widget-state
  variants were nested under e.g. onDark/onBreakpoint, or wrapped in
  NotVariant, never activated because StyleBuilder attached no
  MixInteractionDetector)
- Pointer pressed state has a single owner (MixInteractionDetector's
  Listener); the duplicate GestureDetector tapDown/tapUp/tapCancel writers
  are removed
- Keyboard activation models held state: pressed on key down, activate once
  on key up, repeats suppressed, cancellation on focus loss, disable,
  controller swap, and dispose
- Pressable handles WidgetStatesController swaps in didUpdateWidget and no
  longer risks disposing an external controller or leaking its own
- Semantics expose enabled state, gate tap/longPress on enabled, and no
  longer duplicate actions through GestureDetector's implicit semantics
- PressableBox forwards the full Pressable surface, including the
  previously dropped enableFeedback

BREAKING CHANGE: semanticButtonLabel is renamed to semanticsLabel; the
deprecated onKey callback is removed (use onKeyEvent); Pressable reserves
Space, Enter, and numpad Enter for activation, so custom actions no longer
receive ActivateIntent for those keys. onKeyEvent runs first and can still
override activation.
@leoafarias

Copy link
Copy Markdown
Member Author

Superseded by the split into #1006 (nested widget-state discovery fix, independently landable) and #1007 (Pressable input/semantics rework + focus-visible, stacked on #1006). Same combined tree, reorganized for review.

@leoafarias leoafarias closed this Aug 4, 2026
@leoafarias
leoafarias deleted the feat/pressable-focus-visible-semantics branch August 4, 2026 23:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant