Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions packages/mix/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
## Unreleased

### New features

- **Typed focus-visible variants:** Added `FocusVisibleVariant`,
`ContextVariant.focusVisible()`, and `onFocusVisible(...)`. Context variants
can now declare their required states through
`ContextVariant.widgetStateDependencies`.
- **Pressable semantics roles:** Added `PressableSemanticsRole` with button,
link, and neutral roles. `PressableBox` now forwards the full Pressable
focus, keyboard, controller, feedback, cursor, action, and semantics surface.

### Breaking changes

- **Pressable input and semantics:** Replaced `semanticButtonLabel` with
`semanticsLabel`, added `semanticsRole`, and removed the deprecated `onKey`
callback. Use `onKeyEvent` for custom keyboard handling.
- **Reserved activation keys:** Pressable owns Space, Enter, and numpad Enter
so it can model held-key state consistently. Custom activation behavior must
use `onKeyEvent`; custom `actions` remain supported for other intents.

### Fixes

- **Nested widget-state discovery:** `StyleBuilder` now discovers state
requirements recursively through nested and negated variants with
identity-based cycle protection.
- **Pressable lifecycle:** Pointer state has one owner, keyboard activation
fires once on key-up, cancellation clears held state, focus-visible follows
Flutter input modality, and disabled semantics expose no actions.

## 2.2.0-beta.1

### New features
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

/// Provides the current Flutter focus-highlight mode to descendants.
@internal
class FocusHighlightModeProvider extends StatefulWidget {
const FocusHighlightModeProvider({super.key, required this.child});

static FocusHighlightMode of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<_FocusHighlightModeScope>()
?.mode ??
FocusManager.instance.highlightMode;
}

final Widget child;

@override
State<FocusHighlightModeProvider> createState() =>
_FocusHighlightModeProviderState();
}

class _FocusHighlightModeProviderState
extends State<FocusHighlightModeProvider> {
late FocusHighlightMode _mode;

@override
void initState() {
super.initState();
_mode = FocusManager.instance.highlightMode;
FocusManager.instance.addHighlightModeListener(_handleModeChange);
}

void _handleModeChange(FocusHighlightMode mode) {
if (!mounted || mode == _mode) return;

setState(() => _mode = mode);
}

@override
void dispose() {
FocusManager.instance.removeHighlightModeListener(_handleModeChange);
super.dispose();
}

@override
Widget build(BuildContext context) {
return _FocusHighlightModeScope(mode: _mode, child: widget.child);
}
}

class _FocusHighlightModeScope extends InheritedWidget {
const _FocusHighlightModeScope({required this.mode, required super.child});

final FocusHighlightMode mode;

@override
bool updateShouldNotify(_FocusHighlightModeScope oldWidget) {
return mode != oldWidget.mode;
}
}
26 changes: 22 additions & 4 deletions packages/mix/lib/src/core/style.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:collection';

import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

Expand Down Expand Up @@ -66,10 +68,26 @@ abstract class Style<S extends Spec<S>> extends Mix<StyleSpec<S>>

@internal
Set<WidgetState> get widgetStates {
return ($variants ?? [])
.where((v) => v.variant is WidgetStateVariant)
.map((v) => (v.variant as WidgetStateVariant).state)
.toSet();
final states = <WidgetState>{};
final visited = HashSet<Style<S>>.identity();

void collectDependencies(Style<S> style) {
if (!visited.add(style)) return;

final variants = style.$variants;
if (variants == null) return;

for (final variantStyle in variants) {
if (variantStyle.variant case final ContextVariant variant) {
states.addAll(variant.widgetStateDependencies);
}
collectDependencies(variantStyle.value);
}
}

collectDependencies(this);

return states;
}

/// Merges all active variants with their nested variants recursively.
Expand Down
Loading
Loading