material_3_expressive/packages/split_button_m3e/README.md

207 lines
6.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# split_button_m3e
Material 3 Expressive Split Button for Flutter. Two-segment control:
- Leading: primary action (icon, label, or both)
- Trailing: menu trigger (chevron)
All sizes, paddings, radii, and offsets are token-driven, aligned to measurement boards.
## Quick start
```dart
import 'package:split_button_m3e/split_button_m3e.dart';
SplitButtonM3E<String>(
size: SplitButtonM3ESize.md,
shape: SplitButtonM3EShape.round,
emphasis: SplitButtonM3EEmphasis.tonal,
label: 'Save',
leadingIcon: Icons.save_outlined,
onPressed: () => debugPrint('Primary pressed'),
items: const [
SplitButtonM3EItem<String>(value: 'draft', child: 'Save as draft'),
SplitButtonM3EItem<String>(value: 'close', child: 'Save & close'),
],
onSelected: (v) => debugPrint('Selected: $v'),
// Optional tooltips help with semantics and tests
leadingTooltip: 'Save',
trailingTooltip: 'Open menu',
);
```
## Behavior and layout
- Two segments with a fixed inner gap of 2dp.
- Trailing chevron rotates 180° when the menu is open.
- Menu opens aligned to the trailing edge of the arrow button (right edge in LTR, left in RTL).
- Optical chevron offset is applied only in the unselected (closed) state for asymmetrical layout.
- Pressed/expanded shape morph follows the expressive M3 pattern:
- MD/LG/XL: when shape = round and arrow is pressed or menu is open, the trailing segment morphs into a perfect circle (diameter = control height), no inner padding, no optical offset.
- XS/SM: no circle morph in selected state. The selected trailing segment uses a fixed total width of 48dp with side paddings of 13dp.
- Each segment maintains a minimum touch target of 48dp.
## Tokens (by size)
Heights
- XS 32 · S 40 · M 56 · L 96 · XL 136
Trailing width (centered chevron)
- XS 22 · S 22 · M 26 · L 38 · XL 50
Inner gap (between segments)
- 2dp
Inner corner radius (facing edges)
- XS 4 · S 4 · M 4 · L 8 · XL 12
Icon sizes
- XS 20 · S 24 · M 24 · L 32 · XL 40
Optical chevron offset (unselected/resting)
- XS 1 · S 1 · M 2 · L 3 · XL 6
Asymmetrical (unselected) paddings and blocks
- XS: leadingIconBlock 20, leftOuter 12, gap icon→label 4, labelRight 10, trailingLeftInner 12, rightOuter 14
- S: leadingIconBlock 20, leftOuter 16, gap 8, labelRight 12, trailingLeftInner 12, rightOuter 14
- M: leadingIconBlock 24, leftOuter 24, gap 8, labelRight 24, trailingLeftInner 13, rightOuter 17
- L: leadingIconBlock 32, leftOuter 48, gap 12, labelRight 48, trailingLeftInner 26, rightOuter 32
- XL: leadingIconBlock 40, leftOuter 64, gap 16, labelRight 64, trailingLeftInner 37, rightOuter 49
Symmetrical (selected) trailing segment
- Trailing width (centered chevron) + side padding ×2
- Side padding per size: XS 13 · S 13 · M 15 · L 29 · XL 43
- Special case: XS/SM selected total width is 48 (22 + 13 + 13) with 13dp side padding; no full rounding.
Pressed morph radii
- Per-size pressed radius tokens are applied to the pressed segment; when round and MD/LG/XL, trailing becomes a circle while pressed/open.
## API summary
Props
- size: SplitButtonM3ESize (xs, sm, md, lg, xl)
- shape: SplitButtonM3EShape (round, square)
- emphasis: SplitButtonM3EEmphasis (filled, tonal, elevated, outlined, text)
- label: String? (leading segment)
- leadingIcon: IconData? (leading segment icon)
- onPressed: VoidCallback? (leading action)
- items: List<SplitButtonM3EItem<T>>? (trailing menu items), or
- menuBuilder: List<PopupMenuEntry<T>> Function(BuildContext)?
- onSelected: ValueChanged<T>? (when an item is chosen)
- trailingAlignment: SplitButtonM3ETrailingAlignment (opticalCenter, geometricCenter)
- leadingTooltip, trailingTooltip: String? (for semantics/UX)
- enabled: bool (default true)
Items
```dart
const SplitButtonM3EItem<T>({
required T value,
required Object child, // plain string or Widget
bool enabled = true,
});
```
## Accessibility
- Each segment is independently focusable; minimum 48×48dp hit target.
- Tooltips provide accessible names; you can supply copy like “Open menu”.
- Chevron rotation and selected state are conveyed via menu open/close.
## Example (menuBuilder)
```dart
SplitButtonM3E<int>(
size: SplitButtonM3ESize.md,
shape: SplitButtonM3EShape.square,
label: 'More',
leadingIcon: Icons.more_horiz,
onPressed: () => debugPrint('Primary'),
menuBuilder: (context) => [
const PopupMenuItem<int>(value: 1, child: Text('Option 1')),
const PopupMenuItem<int>(value: 2, child: Text('Option 2')),
],
onSelected: (v) => debugPrint('Picked $v'),
trailingAlignment: SplitButtonM3ETrailingAlignment.opticalCenter,
);
```
## Notes
- Menu aligns to the trailing arrows edge (LTR right, RTL left).
- Optical centering is applied only when the menu is closed (unselected asymmetrical state).
- When shape=round and size is MD/LG/XL, the trailing segment becomes a perfect circle while pressed/open; XS/SM remain rectangular with the selected geometry (48 total width, 13 side padding).
## License
MIT
---
## Live demo (Gallery)
Explore this component in the M3E Gallery (GitHub Pages):
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
SplitButtonM3E: a two-part button with a primary action and a dropdown menu, with sizes, variants, shapes, and accessible minimum hit targets. Keyboard navigation supported.
### Installation
- Monorepo (local path): already configured alongside m3e_design.
- Pub (when published):
```yaml
dependencies:
split_button_m3e: ^0.1.0
m3e_design: ^0.1.0
```
Minimum SDK: Dart >=3.9.2; Flutter >=1.17.0.
### Dependencies
- flutter
- m3e_design
### Quick start
```dart
SplitButtonM3E(
label: const Text('Share'),
primaryAction: () { /* do default share */ },
menuItems: const [
SplitButtonItemM3E(label: Text('Copy link'), value: 'copy'),
SplitButtonItemM3E(label: Text('Email'), value: 'email'),
],
onSelected: (value) {
// handle from menu
},
)
```
### Key parameters
- label: Widget — Visible label next to the caret.
- primaryAction: VoidCallback — Action when the main segment is tapped.
- menuItems: List<SplitButtonItemM3E> — Menu options.
- onSelected: ValueChanged<T> — Callback when a menu item is chosen.
- variant/style: filled | tonal | elevated | outlined.
- size: xs | sm | md | lg | xl.
- shapeFamily: round | square.
- isExpanded: bool — Whether to take full width when allowed.
### Accessibility
- Both segments meet 48×48dp minimum size; keyboard and screen reader friendly.
### Links
- Repository: https://github.com/EmilyMoonstone/material_3_expressive/tree/main/packages/split_button_m3e
- Issue tracker: https://github.com/EmilyMonestone/material_3_expressive/issues
- Changelog: ./CHANGELOG.md