diff --git a/packages/remix/CHANGELOG.md b/packages/remix/CHANGELOG.md index cb4b6ad9..ec814bb5 100644 --- a/packages/remix/CHANGELOG.md +++ b/packages/remix/CHANGELOG.md @@ -31,8 +31,13 @@ the shared `TextFieldStyler` callable constructs a single-line `RemixTextField`. Pass a `TextFieldStyler` to `RemixTextArea.style` instead. - **FIX**: Top-align multiline TextField hints and expose label, hint, helper, - error, and interactive accessory semantics once without narrowing the - existing composite tap target. + error, and interactive accessory semantics once. The composite gesture tap + target is unchanged, but the field's semantic bounds now cover the editable + area rather than the full container box; restoring full-box assistive-tech + bounds needs an upstream naked_ui hook and is tracked there. +- **FIX**: Clear TextField hovered styling when the field is disabled while a + pointer is over it. The hover region unmounts without an exit event, so the + state previously leaked across a disable/re-enable cycle. - **FEAT**: Add an optional styled `RemixCheckbox.label` inside the checkbox's pointer, focus, and single semantics target, with a 48-by-48 default minimum target and an explicit `Size.zero` compact opt-out. Mix now generates the diff --git a/packages/remix/lib/src/components/textfield/textfield_widget.dart b/packages/remix/lib/src/components/textfield/textfield_widget.dart index e33f0094..002fd625 100644 --- a/packages/remix/lib/src/components/textfield/textfield_widget.dart +++ b/packages/remix/lib/src/components/textfield/textfield_widget.dart @@ -328,6 +328,13 @@ class _RemixTextFieldBodyState extends State<_RemixTextFieldBody> { _activePressSources.clear(); _styleController.update(.pressed, false); } + // Deliberate: the hover MouseRegion is unmounted while disabled and an + // unmounted MouseRegion never fires onExit, so the hovered flag must be + // cleared here. A pointer still over the field re-acquires it on re-enable + // because MouseTracker dispatches enter events to newly mounted regions. + if (!widget.config.enabled) { + _styleController.update(.hovered, false); + } } void _updatePressSource(_RemixTextFieldPressSource source, bool pressed) { diff --git a/packages/remix/test/components/textfield/textfield_widget_test.dart b/packages/remix/test/components/textfield/textfield_widget_test.dart index 79403717..c3409ecb 100644 --- a/packages/remix/test/components/textfield/textfield_widget_test.dart +++ b/packages/remix/test/components/textfield/textfield_widget_test.dart @@ -1065,6 +1065,53 @@ void main() { expect(cursorColor(), Colors.blue); }); + testWidgets('disabling while hovered clears hovered styling', ( + tester, + ) async { + var enabled = true; + late StateSetter rebuild; + + await tester.pumpRemixApp( + StatefulBuilder( + builder: (context, setState) { + rebuild = setState; + return RemixTextField( + label: 'Hover target', + enabled: enabled, + style: TextFieldStyler( + cursorColor: Colors.blue, + ).onHovered(TextFieldStyler(cursorColor: Colors.red)), + ); + }, + ), + ); + await tester.pump(); + + Color? cursorColor() => tester + .widget(find.byType(NakedTextField)) + .cursorColor; + final mouse = await tester.createGesture(kind: PointerDeviceKind.mouse); + addTearDown(mouse.removePointer); + await mouse.addPointer(location: Offset.zero); + + await mouse.moveTo(tester.getCenter(find.text('Hover target'))); + await tester.pump(); + expect(cursorColor(), Colors.red); + + rebuild(() => enabled = false); + await tester.pump(); + await mouse.moveTo(const Offset(700, 500)); + await tester.pump(); + + rebuild(() => enabled = true); + await tester.pump(); + expect(cursorColor(), Colors.blue); + + await mouse.moveTo(tester.getCenter(find.text('Hover target'))); + await tester.pump(); + expect(cursorColor(), Colors.red); + }); + testWidgets('fallback press down, up, and cancel drive pressed styling', ( tester, ) async {