Add ButtonGroupM3E component with overflow handling and update API to use actions
This commit is contained in:
parent
4ee55ee2aa
commit
582af894f3
14 changed files with 1463 additions and 255 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:m3e_collection/m3e_collection.dart';
|
import 'package:m3e_collection/m3e_collection.dart';
|
||||||
|
import 'package:m3e_gallery/sections/button_group_section.dart';
|
||||||
import 'package:m3e_gallery/sections/button_section.dart';
|
import 'package:m3e_gallery/sections/button_section.dart';
|
||||||
import 'package:m3e_gallery/sections/fab_section.dart';
|
import 'package:m3e_gallery/sections/fab_section.dart';
|
||||||
import 'package:m3e_gallery/sections/icon_button_section.dart';
|
import 'package:m3e_gallery/sections/icon_button_section.dart';
|
||||||
|
|
@ -124,6 +125,7 @@ class SectionedGallery extends StatelessWidget {
|
||||||
return ListView(
|
return ListView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
children: [
|
children: [
|
||||||
|
const ButtonGroupSection(),
|
||||||
const IconButtonSection(),
|
const IconButtonSection(),
|
||||||
const SplitButtonSection(),
|
const SplitButtonSection(),
|
||||||
const ButtonSection(),
|
const ButtonSection(),
|
||||||
|
|
|
||||||
168
apps/gallery/lib/sections/button_group_section.dart
Normal file
168
apps/gallery/lib/sections/button_group_section.dart
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:m3e_collection/m3e_collection.dart';
|
||||||
|
|
||||||
|
import 'section_card.dart';
|
||||||
|
|
||||||
|
class ButtonGroupSection extends StatelessWidget {
|
||||||
|
const ButtonGroupSection({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SectionCard(
|
||||||
|
title: 'ButtonGroupM3E — basic (menu overflow default)',
|
||||||
|
child: _demoBasic(context),
|
||||||
|
),
|
||||||
|
SectionCard(
|
||||||
|
title: 'ButtonGroupM3E — scroll overflow',
|
||||||
|
child: _demoScroll(context),
|
||||||
|
),
|
||||||
|
SectionCard(
|
||||||
|
title: 'ButtonGroupM3E — wrap layout',
|
||||||
|
child: _demoWrap(context),
|
||||||
|
),
|
||||||
|
SectionCard(
|
||||||
|
title: 'ButtonGroupM3E — connected',
|
||||||
|
child: _demoConnected(context),
|
||||||
|
),
|
||||||
|
SectionCard(
|
||||||
|
title: 'ButtonGroupM3E — connected with selection',
|
||||||
|
child: _demoConnectedSelection(context),
|
||||||
|
),
|
||||||
|
SectionCard(
|
||||||
|
title: 'ButtonGroupM3E — connected, menu overflow',
|
||||||
|
child: _demoConnectedMenu(context),
|
||||||
|
),
|
||||||
|
SectionCard(
|
||||||
|
title: 'ButtonGroupM3E — vertical, menu overflow',
|
||||||
|
child: _demoVertical(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _demoBasic(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 280,
|
||||||
|
child: ButtonGroupM3E(
|
||||||
|
actions: [
|
||||||
|
for (final label in [
|
||||||
|
'One',
|
||||||
|
'Two',
|
||||||
|
'Three',
|
||||||
|
'Four',
|
||||||
|
'Five Button',
|
||||||
|
'Six'
|
||||||
|
])
|
||||||
|
ButtonGroupM3EAction(label: Text(label), onPressed: () {}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _demoScroll(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 280,
|
||||||
|
child: ButtonGroupM3E(
|
||||||
|
overflow: ButtonGroupM3EOverflow.scroll,
|
||||||
|
actions: [
|
||||||
|
for (final label in ['A', 'B', 'C', 'D', 'E', 'F'])
|
||||||
|
ButtonGroupM3EAction(
|
||||||
|
label: Text('Item $label'),
|
||||||
|
onPressed: () {},
|
||||||
|
style: ButtonM3EStyle.filled,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _demoWrap(BuildContext context) {
|
||||||
|
return ButtonGroupM3E(
|
||||||
|
wrap: true,
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
actions: [
|
||||||
|
for (final i in List.generate(10, (i) => i))
|
||||||
|
ButtonGroupM3EAction(label: Text('Wrap $i'), onPressed: () {}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _demoConnected(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 300,
|
||||||
|
child: ButtonGroupM3E(
|
||||||
|
type: ButtonGroupM3EType.connected,
|
||||||
|
dividerColor: Theme.of(context).colorScheme.outlineVariant,
|
||||||
|
style: ButtonM3EStyle.tonal,
|
||||||
|
actions: [
|
||||||
|
for (final label in ['Low', 'Med', 'High'])
|
||||||
|
ButtonGroupM3EAction(
|
||||||
|
label: Text(label),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _demoConnectedSelection(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 300,
|
||||||
|
child: ButtonGroupM3E(
|
||||||
|
type: ButtonGroupM3EType.connected,
|
||||||
|
dividerColor: Theme.of(context).colorScheme.outlineVariant,
|
||||||
|
style: ButtonM3EStyle.tonal,
|
||||||
|
selection: true,
|
||||||
|
actions: [
|
||||||
|
for (final label in ['Low', 'Med', 'High'])
|
||||||
|
ButtonGroupM3EAction(
|
||||||
|
label: Text(label),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _demoConnectedMenu(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 300,
|
||||||
|
child: ButtonGroupM3E(
|
||||||
|
type: ButtonGroupM3EType.connected,
|
||||||
|
dividerColor: Theme.of(context).colorScheme.outlineVariant,
|
||||||
|
style: ButtonM3EStyle.tonal,
|
||||||
|
actions: [
|
||||||
|
for (final label in [
|
||||||
|
'One',
|
||||||
|
'Two',
|
||||||
|
'Three',
|
||||||
|
'Four',
|
||||||
|
'Five Button',
|
||||||
|
'Six'
|
||||||
|
])
|
||||||
|
ButtonGroupM3EAction(
|
||||||
|
label: Text(label),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _demoVertical(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 160,
|
||||||
|
child: ButtonGroupM3E(
|
||||||
|
direction: Axis.vertical,
|
||||||
|
actions: [
|
||||||
|
for (final label in ['Top', 'Middle', 'Bottom', 'Extra'])
|
||||||
|
ButtonGroupM3EAction(label: Text(label), onPressed: () {}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,2 +1,31 @@
|
||||||
|
## 0.3.0
|
||||||
|
- Breaking: Removed legacy `children` parameter. Use `actions: List<ButtonGroupM3EAction>` instead.
|
||||||
|
- Breaking: Renamed `groupSelection` API to `selection` for clarity.
|
||||||
|
- Added `ButtonGroupM3EAction` as the sole way to define buttons in a group.
|
||||||
|
- Added connected selection corner logic: only outer ends round; inner corners use token square radius; selected buttons fully round.
|
||||||
|
- Added 2px gap between connected buttons (when `showDividers=false`) and before overflow trigger.
|
||||||
|
- Added per-corner shape application via new `cornerRadiusOverride` in `ButtonM3E` to support mixed rounded/square corners in groups.
|
||||||
|
- Added new anchored dropdown overflow menu (default) using `overflowMenuStyle: ButtonGroupM3EOverflowMenuStyle.dropdown`; bottom sheet alternative retained (`bottomSheet`).
|
||||||
|
- Overflow trigger now rendered as a `ButtonM3E` for visual consistency.
|
||||||
|
- Improved overflow fitting algorithm (exact iterative width packing + epsilon) to eliminate minor RenderFlex pixel overflows.
|
||||||
|
- Added right-alignment support: maps `crossAxisAlignment` for linear layouts and allows popup buttons to align right.
|
||||||
|
- Misc: Refined measurement pass; popup menu intrinsic width matches widest action; dismissal on outside tap.
|
||||||
|
|
||||||
|
## 0.2.0
|
||||||
|
- New: Overflow handling options for non-wrapping groups via `overflow` parameter:
|
||||||
|
- `menu` (new default): show as many children as fit and place the rest in a bottom sheet opened by a trailing overflow icon.
|
||||||
|
- `scroll`: allow scrolling along the axis under bounded constraints.
|
||||||
|
- `none`: no special handling.
|
||||||
|
- Behavior: The default overflow changed from `scroll` to `menu` for a better UX in constrained layouts.
|
||||||
|
- Implementation: Converted `ButtonGroupM3E` to a StatefulWidget to measure child extents and avoid overflow.
|
||||||
|
- Tests: Added tests for scroll, wrap, vertical, and menu overflow scenarios.
|
||||||
|
|
||||||
|
## 0.1.2
|
||||||
|
- Prevent RenderFlex overflow in non-wrapping groups by auto-enabling scroll when axis constraints are bounded.
|
||||||
|
- Added widget tests covering horizontal/vertical bounded scenarios and wrap behavior.
|
||||||
|
|
||||||
|
## 0.1.1
|
||||||
|
- Minor internal adjustments.
|
||||||
|
|
||||||
## 0.1.0
|
## 0.1.0
|
||||||
- Changelog initialized.
|
- Changelog initialized.
|
||||||
|
|
|
||||||
|
|
@ -1,173 +1,72 @@
|
||||||
# button_group_m3e
|
# button_group_m3e
|
||||||
|
|
||||||
Wrapper-only **Button Group** for Material 3 Expressive (M3E).
|
Material 3 Expressive grouped button layout and overflow management.
|
||||||
Arranges arbitrary action buttons and applies **group-level presentation**: type (standard/connected), shape family (round/square), size (XS–XL), density, and layout (axis, wrap).
|
|
||||||
|
|
||||||
> Buttons themselves remain independent (no selection logic). Use your own M3E buttons (`icon_button_m3e`, `split_button_m3e`, etc.).
|
## Current API (0.3.0)
|
||||||
|
`children` has been removed. Provide `actions: List<ButtonGroupM3EAction>`.
|
||||||
## Install (in monorepo)
|
|
||||||
|
|
||||||
Place this folder alongside `m3e_design`:
|
|
||||||
|
|
||||||
```
|
|
||||||
packages/
|
|
||||||
m3e_design/
|
|
||||||
button_group_m3e/
|
|
||||||
```
|
|
||||||
|
|
||||||
`pubspec.yaml` already expects `m3e_design` at `../m3e_design`.
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
class ButtonGroupM3E extends StatelessWidget {
|
ButtonGroupM3E(
|
||||||
const ButtonGroupM3E({
|
actions: [
|
||||||
required List<Widget> children,
|
ButtonGroupM3EAction(label: const Text('One'), onPressed: () {}),
|
||||||
ButtonGroupM3EType type = ButtonGroupM3EType.standard,
|
ButtonGroupM3EAction(label: const Text('Two'), onPressed: () {}),
|
||||||
ButtonGroupM3EShape shape = ButtonGroupM3EShape.round,
|
ButtonGroupM3EAction(label: const Text('Three'), onPressed: () {}),
|
||||||
ButtonGroupM3ESize size = ButtonGroupM3ESize.md,
|
],
|
||||||
ButtonGroupM3EDensity density = ButtonGroupM3EDensity.regular,
|
overflow: ButtonGroupM3EOverflow.menu, // default
|
||||||
Axis direction = Axis.horizontal,
|
)
|
||||||
bool wrap = false,
|
```
|
||||||
double? spacing,
|
|
||||||
double? runSpacing,
|
## Actions
|
||||||
WrapAlignment alignment = WrapAlignment.start,
|
```dart
|
||||||
WrapAlignment runAlignment = WrapAlignment.start,
|
class ButtonGroupM3EAction {
|
||||||
WrapCrossAlignment crossAxisAlignment = WrapCrossAlignment.center,
|
const ButtonGroupM3EAction({
|
||||||
bool showDividers = false,
|
required Widget label,
|
||||||
Color? dividerColor,
|
Widget? icon,
|
||||||
double? dividerThickness,
|
VoidCallback? onPressed,
|
||||||
bool equalizeWidths = false,
|
bool enabled = true,
|
||||||
String? semanticLabel,
|
ButtonM3EStyle style = ButtonM3EStyle.filled,
|
||||||
Clip clipBehavior = Clip.none,
|
bool toggleable = false,
|
||||||
|
bool selected = false,
|
||||||
|
ValueChanged<bool>? onSelectedChange,
|
||||||
|
ButtonM3EShape? shape,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Enums:
|
## Group selection
|
||||||
```dart
|
Enable segmented styling:
|
||||||
enum ButtonGroupM3EType { standard, connected }
|
|
||||||
enum ButtonGroupM3EShape { round, square }
|
|
||||||
enum ButtonGroupM3ESize { xs, sm, md, lg, xl }
|
|
||||||
enum ButtonGroupM3EDensity { regular, compact }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Scope for cooperative buttons
|
|
||||||
|
|
||||||
The group exposes: `ButtonGroupM3EScope` and `ButtonGroupM3EItemScope`:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
final g = ButtonGroupM3EScope.of(context);
|
|
||||||
final i = ButtonGroupM3EItemScope.of(context);
|
|
||||||
// g.size, g.shape, g.isConnected, g.direction ...
|
|
||||||
// i.index, i.count, i.isFirst, i.isLast ...
|
|
||||||
```
|
|
||||||
|
|
||||||
Buttons can read these to adopt **height, corner radii (outer vs inner), compact paddings**, etc.
|
|
||||||
|
|
||||||
## Defaults (recommended)
|
|
||||||
|
|
||||||
- type: `standard`
|
|
||||||
- shape: `round`
|
|
||||||
- size: `md`
|
|
||||||
- density: `regular`
|
|
||||||
- direction: `Axis.horizontal`
|
|
||||||
- wrap: `false`
|
|
||||||
- standard spacing: token-based (≈8dp at md)
|
|
||||||
- connected spacing: `0`
|
|
||||||
- dividers: `false` by default (connected only)
|
|
||||||
- dividerThickness: `1dp` (hairline)
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- In **wrap** mode, connected **dividers** are not auto-drawn per run (Flutter Wrap lacks per-run hooks). Use standard type, or accept flush seams.
|
|
||||||
- `equalizeWidths` uses min-widths by size (40, 56, 72, 96, 120). For true equalization per run, implement a custom multi-pass layout if needed.
|
|
||||||
- The widget does **not** clip children unless `clipBehavior` is set. Prefer cooperative styling via scope.
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
|
int selectedIndex = 0;
|
||||||
ButtonGroupM3E(
|
ButtonGroupM3E(
|
||||||
type: ButtonGroupM3EType.connected,
|
groupSelection: true,
|
||||||
shape: ButtonGroupM3EShape.round,
|
selectedIndex: selectedIndex,
|
||||||
size: ButtonGroupM3ESize.lg,
|
actions: [
|
||||||
showDividers: true,
|
ButtonGroupM3EAction(label: const Text('Day'), onPressed: () => setState(() => selectedIndex = 0)),
|
||||||
semanticLabel: 'Playback controls',
|
ButtonGroupM3EAction(label: const Text('Week'), onPressed: () => setState(() => selectedIndex = 1)),
|
||||||
children: [
|
ButtonGroupM3EAction(label: const Text('Month'), onPressed: () => setState(() => selectedIndex = 2)),
|
||||||
// Your M3E buttons here...
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
Shape rules when `groupSelection` is true:
|
||||||
|
- Selected button: fully round.
|
||||||
|
- First & last (unselected): round.
|
||||||
|
- Middle unselected buttons: square.
|
||||||
|
|
||||||
|
## Overflow
|
||||||
|
- `menu` (default): shows what fits + overflow trigger with remaining actions in a bottom sheet.
|
||||||
|
- `scroll`: scrolls along main axis when constrained.
|
||||||
|
- `none`: no handling (may overflow if parent allows).
|
||||||
|
|
||||||
---
|
## Other parameters
|
||||||
|
- `type`: standard | connected (divider seams, zero spacing)
|
||||||
|
- `shape`: square | round (base shape family)
|
||||||
|
- `size`: xs | sm | md | lg | xl
|
||||||
|
- `density`: regular | compact
|
||||||
|
- Layout: `direction`, `wrap`, `spacing`, `runSpacing`, alignment options.
|
||||||
|
- `equalizeWidths`: enforce min widths per size for even visual rhythm.
|
||||||
|
|
||||||
## Live demo (Gallery)
|
## Versioning
|
||||||
|
0.3.0 – BREAKING: removed `children`. Use `actions`.
|
||||||
|
|
||||||
Explore this component in the M3E Gallery (GitHub Pages):
|
## License
|
||||||
|
See LICENSE.
|
||||||
https://<your-github-username>.github.io/material_3_expressive/
|
|
||||||
|
|
||||||
To run the Gallery locally:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cd apps/gallery
|
|
||||||
flutter run -d chrome
|
|
||||||
```
|
|
||||||
|
|
||||||
_Last updated: 2025-10-23_
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Detailed Guide
|
|
||||||
|
|
||||||
### What this package provides
|
|
||||||
A layout-only ButtonGroupM3E that propagates size/shape to its child buttons and ensures consistent spacing and overflow behavior for Material 3 Expressive buttons.
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
- Monorepo (local path): already configured alongside m3e_design and button_m3e.
|
|
||||||
- Pub (when published):
|
|
||||||
```yaml
|
|
||||||
dependencies:
|
|
||||||
button_group_m3e: ^0.1.0
|
|
||||||
m3e_design: ^0.1.0
|
|
||||||
button_m3e: ^0.1.0
|
|
||||||
```
|
|
||||||
|
|
||||||
Minimum SDK: Dart >=3.5.0.
|
|
||||||
|
|
||||||
### Dependencies
|
|
||||||
- flutter
|
|
||||||
- m3e_design
|
|
||||||
|
|
||||||
### Quick start
|
|
||||||
```dart
|
|
||||||
ButtonGroupM3E(
|
|
||||||
size: ButtonM3ESize.md,
|
|
||||||
shapeFamily: ButtonM3EShapeFamily.round,
|
|
||||||
children: [
|
|
||||||
ButtonM3E.filled(onPressed: () {}, label: const Text('One')),
|
|
||||||
ButtonM3E.outlined(onPressed: () {}, label: const Text('Two')),
|
|
||||||
ButtonM3E.text(onPressed: () {}, label: const Text('Three')),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key parameters
|
|
||||||
- children: List<Widget> — Typically ButtonM3E instances.
|
|
||||||
- size: ButtonM3ESize — xs | sm | md | lg | xl. Propagated to children when possible.
|
|
||||||
- shapeFamily: ButtonM3EShapeFamily — round | square.
|
|
||||||
- spacing: double? — Horizontal/vertical gap between children.
|
|
||||||
- direction / wrap: Axis or wrap behavior depending on implementation.
|
|
||||||
|
|
||||||
### Theming with m3e_design
|
|
||||||
Spacing/shape defaults are derived from M3ETheme tokens; you can override explicitly per group or per button.
|
|
||||||
|
|
||||||
### Accessibility
|
|
||||||
- Maintains minimum tap targets for grouped buttons and preserves focus order.
|
|
||||||
|
|
||||||
### Links
|
|
||||||
- Repository: https://github.com/EmilyMoonstone/material_3_expressive/tree/main/packages/button_group_m3e
|
|
||||||
- Issue tracker: https://github.com/EmilyMonestone/material_3_expressive/issues
|
|
||||||
- Changelog: ./CHANGELOG.md
|
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,5 @@ library button_group_m3e;
|
||||||
export 'src/button_group_m3e_enums.dart';
|
export 'src/button_group_m3e_enums.dart';
|
||||||
export 'src/button_group_m3e_scope.dart'
|
export 'src/button_group_m3e_scope.dart'
|
||||||
show ButtonGroupM3EScope, ButtonGroupM3EItemScope;
|
show ButtonGroupM3EScope, ButtonGroupM3EItemScope;
|
||||||
export 'src/button_group_m3e_widget.dart';
|
export 'src/button_group_m3e_widget.dart'
|
||||||
|
show ButtonGroupM3E, ButtonGroupM3EOverflow, ButtonGroupM3EAction;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -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.1.1
|
version: 0.3.0
|
||||||
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
|
||||||
|
|
||||||
|
|
@ -11,6 +11,7 @@ dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
m3e_design: ^0.2.1
|
m3e_design: ^0.2.1
|
||||||
|
button_m3e: ^0.1.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,5 @@
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
m3e_design:
|
m3e_design:
|
||||||
path: ..\\m3e_design
|
path: ..\\m3e_design
|
||||||
|
button_m3e:
|
||||||
|
path: ..\\button_m3e
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,246 @@
|
||||||
|
import 'package:button_group_m3e/button_group_m3e.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('does not overflow horizontally under bounded width by scrolling',
|
||||||
|
(tester) async {
|
||||||
|
final actions = List<ButtonGroupM3EAction>.generate(
|
||||||
|
8,
|
||||||
|
(i) => ButtonGroupM3EAction(
|
||||||
|
label: SizedBox(width: 120, height: 40, child: Text('A$i')),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width:
|
||||||
|
240, // force bounded width smaller than total children width
|
||||||
|
child: ButtonGroupM3E(
|
||||||
|
actions: actions,
|
||||||
|
// wrap=false by default
|
||||||
|
overflow: ButtonGroupM3EOverflow.scroll,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(tester.takeException(), isNull, reason: 'Should not overflow');
|
||||||
|
expect(find.byType(SingleChildScrollView), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('wrap=true uses Wrap and avoids overflow without scroll',
|
||||||
|
(tester) async {
|
||||||
|
final actions = List<ButtonGroupM3EAction>.generate(
|
||||||
|
8,
|
||||||
|
(i) => ButtonGroupM3EAction(
|
||||||
|
label: SizedBox(width: 120, height: 40, child: Text('B$i')),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 240,
|
||||||
|
child: ButtonGroupM3E(
|
||||||
|
wrap: true,
|
||||||
|
runSpacing: 4,
|
||||||
|
spacing: 4,
|
||||||
|
actions: actions,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(tester.takeException(), isNull,
|
||||||
|
reason: 'Wrap should avoid overflow');
|
||||||
|
expect(find.byType(Wrap), findsOneWidget);
|
||||||
|
expect(find.byType(SingleChildScrollView), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('vertical direction scrolls when height is bounded',
|
||||||
|
(tester) async {
|
||||||
|
final actions = List<ButtonGroupM3EAction>.generate(
|
||||||
|
8,
|
||||||
|
(i) => ButtonGroupM3EAction(
|
||||||
|
label: SizedBox(width: 100, height: 60, child: Text('C$i')),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
height:
|
||||||
|
160, // bounded height smaller than sum of children heights
|
||||||
|
child: ButtonGroupM3E(
|
||||||
|
direction: Axis.vertical,
|
||||||
|
actions: actions,
|
||||||
|
overflow: ButtonGroupM3EOverflow.scroll,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
expect(find.byType(SingleChildScrollView), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('scroll overflow prevents RenderFlex overflow', (tester) async {
|
||||||
|
final actions = List<ButtonGroupM3EAction>.generate(
|
||||||
|
8,
|
||||||
|
(i) => ButtonGroupM3EAction(
|
||||||
|
label: SizedBox(width: 120, height: 40, child: Text('D$i')),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 240,
|
||||||
|
child: ButtonGroupM3E(
|
||||||
|
actions: actions,
|
||||||
|
overflow: ButtonGroupM3EOverflow.scroll,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
expect(find.byType(SingleChildScrollView), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('wrap=true uses Wrap and avoids scroll', (tester) async {
|
||||||
|
final actions = List<ButtonGroupM3EAction>.generate(
|
||||||
|
8,
|
||||||
|
(i) => ButtonGroupM3EAction(
|
||||||
|
label: SizedBox(width: 120, height: 40, child: Text('E$i')),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 240,
|
||||||
|
child: ButtonGroupM3E(
|
||||||
|
wrap: true,
|
||||||
|
runSpacing: 4,
|
||||||
|
spacing: 4,
|
||||||
|
actions: actions,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
expect(find.byType(Wrap), findsOneWidget);
|
||||||
|
expect(find.byType(SingleChildScrollView), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'menu overflow shows trigger and opens sheet with remaining children',
|
||||||
|
(tester) async {
|
||||||
|
final actions = List<ButtonGroupM3EAction>.generate(
|
||||||
|
6,
|
||||||
|
(i) => ButtonGroupM3EAction(
|
||||||
|
label: SizedBox(width: 100, height: 40, child: Text('M$i')),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 250, // Should not fit all 6 at ~100 each
|
||||||
|
child: ButtonGroupM3E(
|
||||||
|
actions: actions,
|
||||||
|
overflow: ButtonGroupM3EOverflow.menu,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Overflow trigger should appear (IconButton default Icons.more_horiz)
|
||||||
|
expect(find.byIcon(Icons.more_horiz), findsOneWidget);
|
||||||
|
|
||||||
|
// Tap overflow trigger
|
||||||
|
await tester.tap(find.byIcon(Icons.more_horiz));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Bottom sheet should appear
|
||||||
|
expect(find.byType(BottomSheet), findsOneWidget);
|
||||||
|
// All items should be accessible either inline or in sheet (texts M0..M5)
|
||||||
|
for (var i = 0; i < actions.length; i++) {
|
||||||
|
expect(find.text('M$i'), findsWidgets);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('menu overflow does not appear if all children fit',
|
||||||
|
(tester) async {
|
||||||
|
final actions = List<ButtonGroupM3EAction>.generate(
|
||||||
|
3,
|
||||||
|
(i) => ButtonGroupM3EAction(
|
||||||
|
label: SizedBox(width: 60, height: 40, child: Text('F$i')),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 400, // Enough for all
|
||||||
|
child: ButtonGroupM3E(
|
||||||
|
actions: actions,
|
||||||
|
overflow: ButtonGroupM3EOverflow.menu,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.byIcon(Icons.more_horiz), findsNothing);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,2 +1,7 @@
|
||||||
|
## 0.1.2
|
||||||
|
- Add optional `cornerRadiusOverride` to allow per-corner radii when using square shapes (enables grouped buttons with rounded outer and square inner corners).
|
||||||
|
- Shape resolution updated to honor `cornerRadiusOverride` for square/pressed-square states; no breaking API changes.
|
||||||
|
- Internal: minor style resolution refactors, no behavior changes for existing usages.
|
||||||
|
|
||||||
## 0.1.0
|
## 0.1.0
|
||||||
- Changelog initialized.
|
- Changelog initialized.
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ class ButtonM3E extends StatefulWidget {
|
||||||
this.smallPaddingDeprecated24 = false,
|
this.smallPaddingDeprecated24 = false,
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
this.statesController,
|
this.statesController,
|
||||||
|
this.cornerRadiusOverride, // new optional per-corner override for square shapes
|
||||||
});
|
});
|
||||||
|
|
||||||
final VoidCallback? onPressed;
|
final VoidCallback? onPressed;
|
||||||
|
|
@ -33,6 +34,7 @@ class ButtonM3E extends StatefulWidget {
|
||||||
final bool smallPaddingDeprecated24;
|
final bool smallPaddingDeprecated24;
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
final WidgetStatesController? statesController;
|
final WidgetStatesController? statesController;
|
||||||
|
final BorderRadius? cornerRadiusOverride;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ButtonM3E> createState() => _ButtonM3EState();
|
State<ButtonM3E> createState() => _ButtonM3EState();
|
||||||
|
|
@ -165,12 +167,17 @@ class _ButtonM3EState extends State<ButtonM3E> {
|
||||||
final selected = states.contains(WidgetState.selected) || widget.selected;
|
final selected = states.contains(WidgetState.selected) || widget.selected;
|
||||||
final pressed = states.contains(WidgetState.pressed);
|
final pressed = states.contains(WidgetState.pressed);
|
||||||
|
|
||||||
|
final BorderRadius squareBaseRadius = widget.cornerRadiusOverride ??
|
||||||
|
BorderRadius.circular(tokens.squareRadius(widget.size));
|
||||||
|
final BorderRadius pressedSquareRadius = widget.cornerRadiusOverride ??
|
||||||
|
BorderRadius.circular(tokens.pressedRadius(widget.size));
|
||||||
|
|
||||||
OutlinedBorder round = const StadiumBorder();
|
OutlinedBorder round = const StadiumBorder();
|
||||||
OutlinedBorder square = RoundedRectangleBorder(
|
OutlinedBorder square = RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(tokens.squareRadius(widget.size)),
|
borderRadius: squareBaseRadius,
|
||||||
);
|
);
|
||||||
OutlinedBorder pressedSquare = RoundedRectangleBorder(
|
OutlinedBorder pressedSquare = RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(tokens.pressedRadius(widget.size)),
|
borderRadius: pressedSquareRadius,
|
||||||
);
|
);
|
||||||
|
|
||||||
OutlinedBorder base = widget.shape == ButtonM3EShape.round ? round : square;
|
OutlinedBorder base = widget.shape == ButtonM3EShape.round ? round : square;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
|
|
||||||
name: button_m3e
|
name: button_m3e
|
||||||
description: Material 3 Expressive Buttons for Flutter with 5 styles, 5 sizes, round/square shapes, and toggle selection.
|
description: Material 3 Expressive Buttons for Flutter with 5 styles, 5 sizes, round/square shapes, and toggle selection.
|
||||||
version: 0.1.1
|
version: 0.1.2
|
||||||
repository: https://github.com/EmilyMoonstone/material_3_expressive/tree/main/packages/button_m3e
|
repository: https://github.com/EmilyMoonstone/material_3_expressive/tree/main/packages/button_m3e
|
||||||
issue_tracker: https://github.com/EmilyMonestone/material_3_expressive/issues
|
issue_tracker: https://github.com/EmilyMonestone/material_3_expressive/issues
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.5
|
version: 0.3.6
|
||||||
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,8 +9,8 @@ environment:
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
app_bar_m3e: ^0.1.2
|
app_bar_m3e: ^0.1.2
|
||||||
button_group_m3e: ^0.1.1
|
button_group_m3e: ^0.3.0
|
||||||
button_m3e: ^0.1.1
|
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
|
||||||
flutter:
|
flutter:
|
||||||
|
|
|
||||||
|
|
@ -190,15 +190,25 @@ Widget buildButtonGroupM3EDefaultUseCase(BuildContext context) {
|
||||||
labelBuilder: (String v) => v,
|
labelBuilder: (String v) => v,
|
||||||
);
|
);
|
||||||
|
|
||||||
late final List<Widget> children;
|
late final List<ButtonGroupM3EAction> actions;
|
||||||
switch (contentMode) {
|
switch (contentMode) {
|
||||||
case 'with_icon':
|
case 'with_icon':
|
||||||
children = _demoButtonsWithIcons(context);
|
actions = _demoButtonsWithIcons(context)
|
||||||
|
.map((b) => ButtonGroupM3EAction(
|
||||||
|
label: (b as ButtonM3E).label,
|
||||||
|
icon: b.icon,
|
||||||
|
onPressed: b.onPressed,
|
||||||
|
style: b.style))
|
||||||
|
.toList();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
children = _demoButtonsWithLabels(context);
|
actions = _demoButtonsWithLabels(context)
|
||||||
|
.map((b) => ButtonGroupM3EAction(
|
||||||
|
label: (b as ButtonM3E).label,
|
||||||
|
onPressed: b.onPressed,
|
||||||
|
style: b.style))
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: ButtonGroupM3E(
|
child: ButtonGroupM3E(
|
||||||
type: type,
|
type: type,
|
||||||
|
|
@ -218,7 +228,7 @@ Widget buildButtonGroupM3EDefaultUseCase(BuildContext context) {
|
||||||
equalizeWidths: equalizeWidths,
|
equalizeWidths: equalizeWidths,
|
||||||
semanticLabel: semanticLabel,
|
semanticLabel: semanticLabel,
|
||||||
clipBehavior: clip,
|
clipBehavior: clip,
|
||||||
children: children,
|
actions: actions,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -236,7 +246,12 @@ Widget buildButtonGroupM3EConnectedUseCase(BuildContext context) {
|
||||||
label: 'equalizeWidths',
|
label: 'equalizeWidths',
|
||||||
initialValue: false,
|
initialValue: false,
|
||||||
),
|
),
|
||||||
children: _demoButtonsWithLabels(context),
|
actions: _demoButtonsWithLabels(context)
|
||||||
|
.map((b) => ButtonGroupM3EAction(
|
||||||
|
label: (b as ButtonM3E).label,
|
||||||
|
onPressed: b.onPressed,
|
||||||
|
style: b.style))
|
||||||
|
.toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -257,7 +272,12 @@ Widget buildButtonGroupM3EVerticalUseCase(BuildContext context) {
|
||||||
max: 32,
|
max: 32,
|
||||||
divisions: 32,
|
divisions: 32,
|
||||||
),
|
),
|
||||||
children: _demoButtonsWithLabels(context),
|
actions: _demoButtonsWithLabels(context)
|
||||||
|
.map((b) => ButtonGroupM3EAction(
|
||||||
|
label: (b as ButtonM3E).label,
|
||||||
|
onPressed: b.onPressed,
|
||||||
|
style: b.style))
|
||||||
|
.toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -271,13 +291,17 @@ Widget buildButtonGroupM3EWrappedManyItemsUseCase(BuildContext context) {
|
||||||
max: 40,
|
max: 40,
|
||||||
divisions: 40,
|
divisions: 40,
|
||||||
);
|
);
|
||||||
final List<Widget> items = List<Widget>.generate(count, (int i) {
|
final List<ButtonGroupM3EAction> actions =
|
||||||
return OutlinedButton(
|
List<ButtonGroupM3EAction>.generate(
|
||||||
onPressed: () => debugPrint('Pressed: Item #$i'),
|
count,
|
||||||
child: Text('Item $i'),
|
(int i) {
|
||||||
);
|
return ButtonGroupM3EAction(
|
||||||
});
|
label: Text('Item $i'),
|
||||||
|
onPressed: () => debugPrint('Pressed: Item #$i'),
|
||||||
|
style: ButtonM3EStyle.outlined,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
return Center(
|
return Center(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 360,
|
width: 360,
|
||||||
|
|
@ -305,7 +329,7 @@ Widget buildButtonGroupM3EWrappedManyItemsUseCase(BuildContext context) {
|
||||||
alignment: _knobWrapAlignment(context, label: 'alignment'),
|
alignment: _knobWrapAlignment(context, label: 'alignment'),
|
||||||
runAlignment: _knobWrapAlignment(context, label: 'runAlignment'),
|
runAlignment: _knobWrapAlignment(context, label: 'runAlignment'),
|
||||||
crossAxisAlignment: _knobCrossAlignment(context),
|
crossAxisAlignment: _knobCrossAlignment(context),
|
||||||
children: items,
|
actions: actions,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -313,21 +337,20 @@ Widget buildButtonGroupM3EWrappedManyItemsUseCase(BuildContext context) {
|
||||||
|
|
||||||
@UseCase(name: 'equalized_long_text', type: ButtonGroupM3E)
|
@UseCase(name: 'equalized_long_text', type: ButtonGroupM3E)
|
||||||
Widget buildButtonGroupM3EEqualizedLongTextUseCase(BuildContext context) {
|
Widget buildButtonGroupM3EEqualizedLongTextUseCase(BuildContext context) {
|
||||||
final List<Widget> children = <Widget>[
|
final actions = <ButtonGroupM3EAction>[
|
||||||
ElevatedButton(
|
ButtonGroupM3EAction(
|
||||||
onPressed: () => debugPrint('Pressed: Very long primary label'),
|
label: const Text('Very long primary label'),
|
||||||
child: const Text('Very long primary label'),
|
onPressed: () => debugPrint('Pressed: Very long primary label'),
|
||||||
),
|
style: ButtonM3EStyle.filled),
|
||||||
OutlinedButton(
|
ButtonGroupM3EAction(
|
||||||
onPressed: () => debugPrint('Pressed: Short'),
|
label: const Text('Short'),
|
||||||
child: const Text('Short'),
|
onPressed: () => debugPrint('Pressed: Short'),
|
||||||
),
|
style: ButtonM3EStyle.outlined),
|
||||||
TextButton(
|
ButtonGroupM3EAction(
|
||||||
onPressed: () => debugPrint('Pressed: Mid length'),
|
label: const Text('Mid length'),
|
||||||
child: const Text('Mid length'),
|
onPressed: () => debugPrint('Pressed: Mid length'),
|
||||||
),
|
style: ButtonM3EStyle.text),
|
||||||
];
|
];
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: ButtonGroupM3E(
|
child: ButtonGroupM3E(
|
||||||
type: _knobType(context),
|
type: _knobType(context),
|
||||||
|
|
@ -335,17 +358,14 @@ Widget buildButtonGroupM3EEqualizedLongTextUseCase(BuildContext context) {
|
||||||
size: _knobSize(context),
|
size: _knobSize(context),
|
||||||
density: _knobDensity(context),
|
density: _knobDensity(context),
|
||||||
equalizeWidths: true,
|
equalizeWidths: true,
|
||||||
children: children,
|
actions: actions,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseCase(name: 'empty', type: ButtonGroupM3E)
|
@UseCase(name: 'empty', type: ButtonGroupM3E)
|
||||||
Widget buildButtonGroupM3EEmptyUseCase(BuildContext context) {
|
Widget buildButtonGroupM3EEmptyUseCase(BuildContext context) {
|
||||||
// Boundary: no children → should layout to zero size.
|
return const Center(child: ButtonGroupM3E(actions: <ButtonGroupM3EAction>[]));
|
||||||
return const Center(
|
|
||||||
child: ButtonGroupM3E(children: <Widget>[]),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseCase(name: 'with_icon', type: ButtonGroupM3E)
|
@UseCase(name: 'with_icon', type: ButtonGroupM3E)
|
||||||
|
|
@ -357,7 +377,13 @@ Widget buildButtonGroupM3EWithIconUseCase(BuildContext context) {
|
||||||
size: _knobSize(context),
|
size: _knobSize(context),
|
||||||
density: _knobDensity(context),
|
density: _knobDensity(context),
|
||||||
direction: _knobDirection(context),
|
direction: _knobDirection(context),
|
||||||
children: _demoButtonsWithIcons(context),
|
actions: _demoButtonsWithIcons(context)
|
||||||
|
.map((b) => ButtonGroupM3EAction(
|
||||||
|
label: (b as ButtonM3E).label,
|
||||||
|
icon: b.icon,
|
||||||
|
onPressed: b.onPressed,
|
||||||
|
style: b.style))
|
||||||
|
.toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue