Add expanded and linearMainAxisAlignment options to ButtonGroupM3E for improved layout control

This commit is contained in:
Emily Pauli 2025-11-12 14:17:00 +01:00
commit 76daccbc71
6 changed files with 101 additions and 27 deletions

View file

@ -39,6 +39,14 @@ class ButtonGroupSection extends StatelessWidget {
title: 'ButtonGroupM3E — vertical, menu overflow', title: 'ButtonGroupM3E — vertical, menu overflow',
child: _demoVertical(context), child: _demoVertical(context),
), ),
SectionCard(
title: 'ButtonGroupM3E — centered (expanded)',
child: _demoAlignedCenter(context),
),
SectionCard(
title: 'ButtonGroupM3E — right aligned (expanded)',
child: _demoAlignedRight(context),
),
], ],
); );
} }
@ -48,14 +56,7 @@ class ButtonGroupSection extends StatelessWidget {
width: 280, width: 280,
child: ButtonGroupM3E( child: ButtonGroupM3E(
actions: [ actions: [
for (final label in [ for (final label in ['One', 'Two', 'Three', 'Four', 'Five', 'Six'])
'One',
'Two',
'Three',
'Four',
'Five Button',
'Six'
])
ButtonGroupM3EAction(label: Text(label), onPressed: () {}), ButtonGroupM3EAction(label: Text(label), onPressed: () {}),
], ],
), ),
@ -97,10 +98,10 @@ class ButtonGroupSection extends StatelessWidget {
child: ButtonGroupM3E( child: ButtonGroupM3E(
type: ButtonGroupM3EType.connected, type: ButtonGroupM3EType.connected,
dividerColor: Theme.of(context).colorScheme.outlineVariant, dividerColor: Theme.of(context).colorScheme.outlineVariant,
style: ButtonM3EStyle.tonal,
actions: [ actions: [
for (final label in ['Low', 'Med', 'High']) for (final label in ['Low', 'Med', 'High'])
ButtonGroupM3EAction( ButtonGroupM3EAction(
style: ButtonM3EStyle.tonal,
label: Text(label), label: Text(label),
onPressed: () {}, onPressed: () {},
), ),
@ -136,15 +137,9 @@ class ButtonGroupSection extends StatelessWidget {
dividerColor: Theme.of(context).colorScheme.outlineVariant, dividerColor: Theme.of(context).colorScheme.outlineVariant,
style: ButtonM3EStyle.tonal, style: ButtonM3EStyle.tonal,
actions: [ actions: [
for (final label in [ for (final label in ['One', 'Two', 'Three', 'Four', 'Five', 'Six'])
'One',
'Two',
'Three',
'Four',
'Five Button',
'Six'
])
ButtonGroupM3EAction( ButtonGroupM3EAction(
style: ButtonM3EStyle.tonal,
label: Text(label), label: Text(label),
onPressed: () {}, onPressed: () {},
), ),
@ -165,4 +160,31 @@ class ButtonGroupSection extends StatelessWidget {
), ),
); );
} }
Widget _demoAlignedCenter(BuildContext context) {
return ButtonGroupM3E(
expanded: true,
linearMainAxisAlignment: MainAxisAlignment.center,
actions: const [
ButtonGroupM3EAction(label: Text('Center 1')),
ButtonGroupM3EAction(label: Text('Center 2')),
ButtonGroupM3EAction(label: Text('Center 3')),
],
);
}
Widget _demoAlignedRight(BuildContext context) {
return ButtonGroupM3E(
expanded: true,
linearMainAxisAlignment: MainAxisAlignment.end,
overflow: ButtonGroupM3EOverflow.menu,
actions: const [
ButtonGroupM3EAction(label: Text('Right 1')),
ButtonGroupM3EAction(label: Text('Right 2')),
ButtonGroupM3EAction(label: Text('Right 3')),
ButtonGroupM3EAction(label: Text('Right 4')),
ButtonGroupM3EAction(label: Text('Right 5')),
],
);
}
} }

View file

@ -1,3 +1,8 @@
## 0.3.1
- New: `expanded` and `linearMainAxisAlignment` options for non-wrap layouts. When `expanded` is true, the group uses `MainAxisSize.max` and aligns items per `linearMainAxisAlignment` (or mapped from `alignment`). This enables internal center/right alignment without extra wrappers.
- Fix: Connected + menu overflow now consistently inserts a 2px gap before the overflow trigger (when `showDividers=false`) and uses a stricter width-fitting algorithm with a small epsilon to eliminate minor RenderFlex overflows.
- UX: Dropdown overflow popup is anchored bottom-right of the overflow button, has intrinsic width to fit its buttons, and aligns its internal buttons to the right.
## 0.3.0 ## 0.3.0
- Breaking: Removed legacy `children` parameter. Use `actions: List<ButtonGroupM3EAction>` instead. - Breaking: Removed legacy `children` parameter. Use `actions: List<ButtonGroupM3EAction>` instead.
- Breaking: Renamed `groupSelection` API to `selection` for clarity. - Breaking: Renamed `groupSelection` API to `selection` for clarity.

View file

@ -98,6 +98,8 @@ class ButtonGroupM3E extends StatefulWidget {
this.selection = false, this.selection = false,
this.selectedIndex, this.selectedIndex,
this.overflowMenuStyle = ButtonGroupM3EOverflowMenuStyle.dropdown, this.overflowMenuStyle = ButtonGroupM3EOverflowMenuStyle.dropdown,
this.expanded = false,
this.linearMainAxisAlignment,
}); });
/// Declarative actions to build buttons. Overrides [children] when not empty. /// Declarative actions to build buttons. Overrides [children] when not empty.
@ -146,6 +148,13 @@ class ButtonGroupM3E extends StatefulWidget {
/// How to display the overflow menu when [overflow] == menu. Defaults to dropdown. /// How to display the overflow menu when [overflow] == menu. Defaults to dropdown.
final ButtonGroupM3EOverflowMenuStyle overflowMenuStyle; final ButtonGroupM3EOverflowMenuStyle overflowMenuStyle;
/// When true, Row/Column uses MainAxisSize.max enabling internal end alignment.
final bool expanded;
/// Explicit mainAxis alignment for non-wrap (Row/Column) layouts.
/// If null falls back to mapping of [alignment] (WrapAlignment) for convenience.
final MainAxisAlignment? linearMainAxisAlignment;
bool get _connected => type == ButtonGroupM3EType.connected; bool get _connected => type == ButtonGroupM3EType.connected;
@override @override
@ -270,14 +279,23 @@ class _ButtonGroupM3EState extends State<ButtonGroupM3E> {
final list = _buildItemList( final list = _buildItemList(
context, spacing, dividerColor, dividerThickness, context, spacing, dividerColor, dividerThickness,
count: _effectiveActions.length); count: _effectiveActions.length);
final mainAlign =
widget.linearMainAxisAlignment ?? _mapWrapToMain(widget.alignment);
final mainSize = widget.expanded ? MainAxisSize.max : MainAxisSize.min;
return widget.direction == Axis.horizontal return widget.direction == Axis.horizontal
? Row( ? Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: mainSize,
mainAxisAlignment:
widget.expanded ? mainAlign : MainAxisAlignment.start,
crossAxisAlignment: _mapCross(widget.crossAxisAlignment), crossAxisAlignment: _mapCross(widget.crossAxisAlignment),
children: list, children: list,
) )
: Column( : Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: mainSize,
mainAxisAlignment:
widget.expanded ? mainAlign : MainAxisAlignment.start,
crossAxisAlignment: _mapCross(widget.crossAxisAlignment), crossAxisAlignment: _mapCross(widget.crossAxisAlignment),
children: list, children: list,
); );
@ -289,6 +307,10 @@ class _ButtonGroupM3EState extends State<ButtonGroupM3E> {
final list = _buildItemList( final list = _buildItemList(
context, spacing, dividerColor, dividerThickness, context, spacing, dividerColor, dividerThickness,
count: _effectiveActions.length); count: _effectiveActions.length);
final mainAlign =
widget.linearMainAxisAlignment ?? _mapWrapToMain(widget.alignment);
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final isBounded = widget.direction == Axis.horizontal final isBounded = widget.direction == Axis.horizontal
@ -297,12 +319,18 @@ class _ButtonGroupM3EState extends State<ButtonGroupM3E> {
final core = widget.direction == Axis.horizontal final core = widget.direction == Axis.horizontal
? Row( ? Row(
mainAxisSize: MainAxisSize.min, mainAxisSize:
widget.expanded ? MainAxisSize.max : MainAxisSize.min,
mainAxisAlignment:
widget.expanded ? mainAlign : MainAxisAlignment.start,
crossAxisAlignment: _mapCross(widget.crossAxisAlignment), crossAxisAlignment: _mapCross(widget.crossAxisAlignment),
children: list, children: list,
) )
: Column( : Column(
mainAxisSize: MainAxisSize.min, mainAxisSize:
widget.expanded ? MainAxisSize.max : MainAxisSize.min,
mainAxisAlignment:
widget.expanded ? mainAlign : MainAxisAlignment.start,
crossAxisAlignment: _mapCross(widget.crossAxisAlignment), crossAxisAlignment: _mapCross(widget.crossAxisAlignment),
children: list, children: list,
); );
@ -444,17 +472,25 @@ class _ButtonGroupM3EState extends State<ButtonGroupM3E> {
} }
final coreChildren = visibleList; final coreChildren = visibleList;
final mainAlign =
widget.linearMainAxisAlignment ?? _mapWrapToMain(widget.alignment);
final core = widget.direction == Axis.horizontal final core = widget.direction == Axis.horizontal
? ClipRect( ? ClipRect(
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize:
widget.expanded ? MainAxisSize.max : MainAxisSize.min,
mainAxisAlignment:
widget.expanded ? mainAlign : MainAxisAlignment.start,
crossAxisAlignment: _mapCross(widget.crossAxisAlignment), crossAxisAlignment: _mapCross(widget.crossAxisAlignment),
children: coreChildren, children: coreChildren,
), ),
) )
: ClipRect( : ClipRect(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize:
widget.expanded ? MainAxisSize.max : MainAxisSize.min,
mainAxisAlignment:
widget.expanded ? mainAlign : MainAxisAlignment.start,
crossAxisAlignment: _mapCross(widget.crossAxisAlignment), crossAxisAlignment: _mapCross(widget.crossAxisAlignment),
children: coreChildren, children: coreChildren,
), ),
@ -765,6 +801,15 @@ class _ButtonGroupM3EState extends State<ButtonGroupM3E> {
} }
} }
MainAxisAlignment _mapWrapToMain(WrapAlignment w) => switch (w) {
WrapAlignment.start => MainAxisAlignment.start,
WrapAlignment.end => MainAxisAlignment.end,
WrapAlignment.center => MainAxisAlignment.center,
WrapAlignment.spaceBetween => MainAxisAlignment.spaceBetween,
WrapAlignment.spaceAround => MainAxisAlignment.spaceAround,
WrapAlignment.spaceEvenly => MainAxisAlignment.spaceEvenly,
};
Widget _buildOverflowTrigger( Widget _buildOverflowTrigger(
BuildContext context, BuildContext context,
double spacing, double spacing,
@ -830,7 +875,6 @@ class _ButtonGroupM3EState extends State<ButtonGroupM3E> {
_removeOverflowEntry(); _removeOverflowEntry();
final overlay = Overlay.of(context, rootOverlay: true); final overlay = Overlay.of(context, rootOverlay: true);
if (overlay == null) return;
_overflowEntry = OverlayEntry( _overflowEntry = OverlayEntry(
builder: (ctx) { builder: (ctx) {

View file

@ -1,6 +1,6 @@
name: button_group_m3e name: button_group_m3e
description: Wrapper-only Button Group for Material 3 Expressive (layout, shape, size propagation). description: Wrapper-only Button Group for Material 3 Expressive (layout, shape, size propagation).
version: 0.3.0 version: 0.3.1
repository: https://github.com/EmilyMoonstone/material_3_expressive/tree/main/packages/button_group_m3e repository: https://github.com/EmilyMoonstone/material_3_expressive/tree/main/packages/button_group_m3e
issue_tracker: https://github.com/EmilyMonestone/material_3_expressive/issues issue_tracker: https://github.com/EmilyMonestone/material_3_expressive/issues

View file

@ -1,2 +1,5 @@
## 0.3.7
- Updated button_group_m3e to 0.3.1.
## 0.1.0 ## 0.1.0
- Changelog initialized. - Changelog initialized.

View file

@ -1,6 +1,6 @@
name: m3e_collection name: m3e_collection
description: Aggregated exports of all Material 3 Expressive components for Flutter. description: Aggregated exports of all Material 3 Expressive components for Flutter.
version: 0.3.6 version: 0.3.7
repository: https://github.com/EmilyMoonstone/material_3_expressive/tree/main/packages/m3e_collection repository: https://github.com/EmilyMoonstone/material_3_expressive/tree/main/packages/m3e_collection
issue_tracker: https://github.com/EmilyMonestone/material_3_expressive/issues issue_tracker: https://github.com/EmilyMonestone/material_3_expressive/issues
@ -9,7 +9,7 @@ environment:
dependencies: dependencies:
app_bar_m3e: ^0.1.2 app_bar_m3e: ^0.1.2
button_group_m3e: ^0.3.0 button_group_m3e: ^0.3.1
button_m3e: ^0.1.2 button_m3e: ^0.1.2
expressive_refresh: ^0.1.2 expressive_refresh: ^0.1.2
fab_m3e: ^0.1.1 fab_m3e: ^0.1.1