Skip to content

Commit ad38919

Browse files
authored
Fix Semantics expanded state not updating in PopupMenuButton and DropdownButton (#183475)
Fixes #183432 ## Description `_isMenuExpanded` is updated without calling `setState()` in both `PopupMenuButton` and `DropdownButton`, so the `Semantics(expanded: _isMenuExpanded)` widget doesn't rebuild. **`PopupMenuButton`**: The `expanded` state never updates at all since there is no `setState()` call anywhere in `PopupMenuButtonState`. **`DropdownButton`**: Currently masked by `_handleFocusChanged`, which happens to call `setState()` whenever focus changes on open/close, but should not rely on this side effect. ### Changes - Wrapped `_isMenuExpanded` assignments with `setState()` in both widgets. - For `DropdownButton`, moved `_isMenuExpanded = false` from `_removeDropdownRoute()` to the `.then()` callback with a `mounted` guard. This avoids unnecessary `setState()` calls when `_removeDropdownRoute()` is invoked during `dispose()` or orientation changes in `build()`, where the menu state isn't actually changing from user interaction. ## Tests Added regression tests for both `PopupMenuButton` and `DropdownButton` to verify the `expanded` semantics state updates correctly when the menu opens and closes. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing.
1 parent edad06c commit ad38919

4 files changed

Lines changed: 138 additions & 4 deletions

File tree

‎packages/flutter/lib/src/material/dropdown.dart‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1401,7 +1401,6 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
14011401

14021402
void _removeDropdownRoute() {
14031403
_dropdownRoute?._dismiss();
1404-
_isMenuExpanded = false;
14051404
_dropdownRoute = null;
14061405
_lastOrientation = null;
14071406
}
@@ -1505,14 +1504,21 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
15051504
focusNode.requestFocus();
15061505
navigator.push(_dropdownRoute!).then<void>((_DropdownRouteResult<T>? newValue) {
15071506
_removeDropdownRoute();
1507+
if (mounted) {
1508+
setState(() {
1509+
_isMenuExpanded = false;
1510+
});
1511+
}
15081512
if (!mounted || newValue == null) {
15091513
return;
15101514
}
15111515
widget.onChanged?.call(newValue.result);
15121516
});
15131517

15141518
widget.onTap?.call();
1515-
_isMenuExpanded = true;
1519+
setState(() {
1520+
_isMenuExpanded = true;
1521+
});
15161522
}
15171523

15181524
// When isDense is true, reduce the height of this button from _kMenuItemHeight to

‎packages/flutter/lib/src/material/popup_menu.dart‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1692,7 +1692,9 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
16921692
// Only show the menu if there is something to show
16931693
if (items.isNotEmpty) {
16941694
widget.onOpened?.call();
1695-
_isMenuExpanded = true;
1695+
setState(() {
1696+
_isMenuExpanded = true;
1697+
});
16961698
showMenu<T?>(
16971699
context: context,
16981700
elevation: widget.elevation,
@@ -1714,12 +1716,14 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
17141716
if (!mounted) {
17151717
return null;
17161718
}
1719+
setState(() {
1720+
_isMenuExpanded = false;
1721+
});
17171722
if (newValue == null) {
17181723
widget.onCanceled?.call();
17191724
return null;
17201725
}
17211726
widget.onSelected?.call(newValue);
1722-
_isMenuExpanded = false;
17231727
});
17241728
}
17251729
}

‎packages/flutter/test/material/dropdown_test.dart‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1440,6 +1440,62 @@ void main() {
14401440
handle.dispose();
14411441
});
14421442

1443+
testWidgets('Dropdown button Semantics expanded state should update when menu opens and closes', (
1444+
WidgetTester tester,
1445+
) async {
1446+
// Regression test for https://github.com/flutter/flutter/issues/183432
1447+
const key = Key('test');
1448+
await tester.pumpWidget(buildFrame(buttonKey: key, onChanged: onChanged));
1449+
1450+
// Before opening: should have expanded state but not be expanded.
1451+
expect(
1452+
tester.getSemantics(find.byType(DropdownButton<String>)),
1453+
matchesSemantics(
1454+
isButton: true,
1455+
hasExpandedState: true,
1456+
label: 'two',
1457+
hasTapAction: true,
1458+
hasFocusAction: true,
1459+
isFocusable: true,
1460+
),
1461+
);
1462+
1463+
// Open the menu.
1464+
await tester.tap(find.text('two'));
1465+
await tester.pumpAndSettle();
1466+
1467+
// While the menu is open, BlockSemantics in ModalBarrier blocks the
1468+
// button's semantics node (making tester.getSemantics return a stale node).
1469+
// Verify the Semantics widget's expanded property directly instead.
1470+
final Semantics expandedSemantics = tester.widget<Semantics>(
1471+
find.descendant(
1472+
of: find.byKey(key),
1473+
matching: find.byWidgetPredicate(
1474+
(Widget widget) => widget is Semantics && widget.properties.expanded != null,
1475+
),
1476+
),
1477+
);
1478+
expect(expandedSemantics.properties.expanded, isTrue);
1479+
1480+
// Close the menu by selecting an item.
1481+
await tester.tap(find.text('one').last);
1482+
await tester.pumpAndSettle();
1483+
1484+
// After closing: should have expanded state but not be expanded.
1485+
expect(
1486+
tester.getSemantics(find.byType(DropdownButton<String>)),
1487+
matchesSemantics(
1488+
isButton: true,
1489+
hasExpandedState: true,
1490+
label: 'two',
1491+
hasTapAction: true,
1492+
hasFocusAction: true,
1493+
isFocusable: true,
1494+
isFocused: true,
1495+
),
1496+
);
1497+
});
1498+
14431499
testWidgets('Dropdown menu includes semantics', (WidgetTester tester) async {
14441500
final semantics = SemanticsTester(tester);
14451501
const key = Key('test');

‎packages/flutter/test/material/popup_menu_test.dart‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,6 +1382,74 @@ void main() {
13821382
semantics.dispose();
13831383
});
13841384

1385+
testWidgets('PopupMenuButton Semantics expanded state updates when menu opens and closes', (
1386+
WidgetTester tester,
1387+
) async {
1388+
// Regression test for https://github.com/flutter/flutter/issues/183432
1389+
const key = Key('test');
1390+
await tester.pumpWidget(
1391+
MaterialApp(
1392+
home: Material(
1393+
child: PopupMenuButton<int>(
1394+
key: key,
1395+
itemBuilder: (BuildContext context) {
1396+
return <PopupMenuItem<int>>[
1397+
const PopupMenuItem<int>(value: 1, child: Text('Item 1')),
1398+
const PopupMenuItem<int>(value: 2, child: Text('Item 2')),
1399+
];
1400+
},
1401+
child: const SizedBox(height: 100.0, width: 100.0, child: Text('XXX')),
1402+
),
1403+
),
1404+
),
1405+
);
1406+
1407+
// Before opening: should have expanded state but not be expanded.
1408+
expect(
1409+
tester.getSemantics(find.byType(PopupMenuButton<int>)),
1410+
matchesSemantics(
1411+
hasExpandedState: true,
1412+
label: 'XXX',
1413+
hasTapAction: true,
1414+
hasFocusAction: true,
1415+
isFocusable: true,
1416+
),
1417+
);
1418+
1419+
// Open the menu.
1420+
await tester.tap(find.text('XXX'));
1421+
await tester.pumpAndSettle();
1422+
1423+
// While the menu is open, BlockSemantics in ModalBarrier blocks the
1424+
// button's semantics node (making tester.getSemantics return a stale node).
1425+
// Verify the Semantics widget's expanded property directly instead.
1426+
final Semantics expandedSemantics = tester.widget<Semantics>(
1427+
find.descendant(
1428+
of: find.byKey(key),
1429+
matching: find.byWidgetPredicate(
1430+
(Widget widget) => widget is Semantics && widget.properties.expanded != null,
1431+
),
1432+
),
1433+
);
1434+
expect(expandedSemantics.properties.expanded, isTrue);
1435+
1436+
// Close the menu by selecting an item.
1437+
await tester.tap(find.text('Item 1').last);
1438+
await tester.pumpAndSettle();
1439+
1440+
// After closing: should have expanded state but not be expanded.
1441+
expect(
1442+
tester.getSemantics(find.byType(PopupMenuButton<int>)),
1443+
matchesSemantics(
1444+
hasExpandedState: true,
1445+
label: 'XXX',
1446+
hasTapAction: true,
1447+
hasFocusAction: true,
1448+
isFocusable: true,
1449+
),
1450+
);
1451+
});
1452+
13851453
testWidgets('PopupMenuItem merges the semantics of its descendants', (WidgetTester tester) async {
13861454
final semantics = SemanticsTester(tester);
13871455
await tester.pumpWidget(

0 commit comments

Comments
 (0)