feat(mix): rework Pressable input and semantics, add focus-visible variant - #1007
Open
leoafarias wants to merge 6 commits into
Open
feat(mix): rework Pressable input and semantics, add focus-visible variant#1007leoafarias wants to merge 6 commits into
leoafarias wants to merge 6 commits into
Conversation
…riant New features: - FocusVisibleVariant, ContextVariant.focusVisible(), and onFocusVisible(), driven by FocusManager.highlightMode through FocusHighlightModeProvider (the same modality signal FocusableActionDetector uses). Builds on ContextVariant.widgetStateDependencies to declare its focus dependency. - PressableSemanticsRole (button/link/none) and semanticsLabel Fixes: - 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 (completes the keyboard story from #314) - 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.
Review follow-up to the Pressable rework. - Only the Pressable holding primary focus claims activation keys, and it returns ignored for keys it cannot act on. FocusNode.hasFocus is also true while a descendant holds focus, so the previous guard swallowed Space and Enter before a nested TextField (or app shortcuts) ever saw them. - Cover every key WidgetsApp maps to ActivateIntent, restoring select and gameButtonA activation for TV remotes and gamepads. Dropping the ActivateIntent binding had left those two keys bound to nothing. - A pointer that drifts past the tap slop stops counting as a press. Removing GestureDetector's tapCancel took the only arena-aware canceller with it, and a scrolled item travels with the pointer, so list items stayed visually pressed for the whole scroll. - Provide the focus-highlight scope wherever widget states are provided, so onFocusVisible resolves and repaints on modality changes outside a Pressable instead of reading a dependency-free fallback. - FocusVisibleVariant honors WidgetStateStyleOverride, matching onFocused so preview tooling can force the focus-visible look. - Only claim a semantic enabled state for something that can be disabled: a role, or an activation callback. Also drops the dead _cancelHeldActivation parameter and guards the method on the held key, so keyboard bookkeeping can no longer clear a pointer-owned press; collapses the key-event branches; and refreshes the skill docs, which still advertised the removed onKey and semanticButtonLabel. Adds 10 regression tests; reverting the lib changes fails 8 of them.
leoafarias
marked this pull request as draft
August 6, 2026 14:07
Pointer and keyboard presses no longer clear each other. Pressable now owns the published pressed state and derives it from both sources, while MixInteractionDetector reports pointer presses through onPressChange and stops writing WidgetState.pressed when its owner combines sources (managesPressedState). - Track the pointer that owns a press, so a second pointer's move, up, or cancel cannot end a press it did not start - Accept a press only for kPrimaryButton across device kinds, matching GestureDetector's primary tap recognizer - Leave modified key chords (alt/control/meta/shift) to application shortcuts, and contain a competing activation key's repeats while another key is held so they cannot escape to an ancestor shortcut - A disabled Pressable ignores custom key handling and installs no custom actions, while keeping the subtree shape stable across enabled changes - Move hover, focus, and pointer press onto a swapped controller instead of dropping them; a held key still never survives the swap - Scope the semantics node with container and pair Focus.includeSemantics with excludeFromSemantics, so nested controls stay separate Adds 11 tests covering pointer ownership, controller swaps, disposal, auxiliary and modified activation keys, competing-key repeats, disabled actions, cross-source press survival, and nested control semantics.
…semantics-focus-visible Brings in the interaction-detector mounting fix: StyleBuilder now installs MixInteractionDetector only for the states it can drive (hovered/pressed). Only CHANGELOG.md conflicted; the lib changes touch disjoint regions and auto-merged. Resolved by keeping both sides' entries, with the incoming widgetStateDependencies and detector-mounting entries ahead of the Pressable ones, and dropping the now-duplicated widgetStateDependencies mention from the focus-visible bullet. The focus-highlight scope still pairs with every widget-state scope: the detector provides both when mounted, _ExternalControllerProvider provides both on the controller path, and a focus-only style mounts no detector but also publishes no focused state, so onFocusVisible resolves the same either way.
leoafarias
marked this pull request as ready for review
August 6, 2026 16:27
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Pressable interaction-subsystem rework extracted from the Tailwind parity branch (
leoafarias/mix-tailwind-compat). Found during parity work, but none of it is Tailwind-specific: standalone input/lifecycle/a11y fixes plus two general features. Replaces #1005 together with its base PR.Stacked on
fix/nested-widget-state-discovery:FocusVisibleVariantdeclares its focus dependency through thewidgetStateDependenciesAPI introduced there. This PR's diff shows only its own changes; it retargets tomainautomatically when the base merges.Bug fixes
GestureDetector(tapDown/tapUp/tapCancel) andMixInteractionDetector'sListenerwrotepressedto 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.ActivateIntent→onPresswith 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, cancellation on focus loss, disable, controller swap, and dispose. RefsPressableBoxnot pressed when using keyboard triggeronPress#314 — the "onPress not firing" half was fixed back then; this completes the missing pressed-state half its title describes.late final _controllerignoredwidget.controllerswaps, anddispose()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.onTapeven when disabled, had no enabled state or longPress action, andGestureDetector's implicit semantics duplicated actions. Actions are now gated onenabled,enabledis exposed, and the GestureDetector is always excluded from semantics.PressableBox.enableFeedbackexisted but was never forwarded toPressable; the full surface (cursor, focus, keyboard, controller, actions, semantics) is now forwarded.New features
FocusVisibleVariant/ContextVariant.focusVisible()/onFocusVisible(...): CSS:focus-visibleequivalent, keyed offFocusManager.highlightMode(the same input-modality signalFocusableActionDetectoruses), made reactive insidePressablevia the new internalFocusHighlightModeProvider.PressableSemanticsRole(button / link / none) +semanticsLabel.mix_protocol's schema inventory registers the new public symbols as v1 out-of-scope.Breaking changes
semanticButtonLabel→semanticsLabel(no remaining usages in-repo).onKeyremoved; useonKeyEvent(it runs first and can override activation).Pressablereserves Space/Enter/numpad Enter: customactionsno longer receiveActivateIntentfor 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.
Issue-tracker context
Searched open and closed issues (Pressable, pressed, focus, keyboard, semantics, accessibility, enableFeedback, focus-visible, …): no open issue reports these bugs or requests these features. #314 (closed, v1-era) is the nearest prior report, referenced above.
Test plan
packages/mixsuite passes (2,852 tests), including new coverage: keyboard hold/activate-once/cancel paths, controller-swap state transfer,onKeyEventprecedence, reserved-keys-vs-custom-actions, focus-visible modality switching, and a semantics role/action matrix (enabled × tap × longPress).packages/mix_protocolinventory + contract tests pass;dart analyzeclean.The Tailwind parity branch consumes this PR (
focus-visible:prefix →onFocusVisible,TwPressable→ semantics roles) and will be rebased once it lands.