Skip to content

LayrzButtonGroup

Kenny Mochizuki Escalona edited this page Aug 20, 2026 · 6 revisions

LayrzButtonGroup

A responsive button group that renders actions as either a row of buttons or a single trigger opening a dropdown menu, depending on viewport width.

Metadata
Mirrors: ThemedActionsButtons (layrz_theme)
Phase: M2 (Core primitives)
Domain: Buttons
Primitive: Wrap + LayrzDropdownMenu (responsive collapse)
Status: Confirmed scope.


Overview

LayrzButtonGroup manages a collection of dropdown items rendered as a row of LayrzButton instances (converted from LayrzDropdownEntry) or as a dropdown menu. On narrow viewports (below the md breakpoint, 960px), it automatically collapses into a single trigger button that opens a dropdown menu listing the items.

Key principle: Items are the source of truth — they drive both row and dropdown modes. Dropdown menu items pass through unchanged; row mode converts LayrzDropdownEntry items to labelled buttons and silently skips LayrzDropdownLabel items.

Design Principles

  • Items as the data model: Takes List<LayrzDropdownItem> (entries and labels), not buttons.
  • Responsive collapse: Automatically switches between row and dropdown modes at viewport breakpoints.
  • Nullable mode switch: A single boolean parameter (useDropdown) gates the behavior; nullability is the switch rather than an enum, preventing invalid combinations.
  • Entry-to-button conversion: LayrzDropdownEntry instances are converted to labelled LayrzButton instances in row mode. Labels are skipped (not rendered in row mode, only in dropdown mode).
  • Semantic color mapping: Entry color dots (from semantic factories or explicit color) map to button color in row mode; buttons always use type: custom.
  • Auto-close on selection: Dropdown entries close the menu automatically after tapping.
  • Stable trigger naming: The trigger requires an explicit accessible name (the triggerHintText parameter), not derived from its contents. Platform overflow menus identify the control, not enumerate its actions.

API Structure

Default Constructor

class LayrzButtonGroup extends StatelessWidget {
  /// The items rendered by this group, in order.
  ///
  /// Must be a list of [LayrzDropdownItem] instances (either [LayrzDropdownEntry]
  /// or [LayrzDropdownLabel]). The items pass directly to the dropdown menu in
  /// dropdown mode. In row mode, only [LayrzDropdownEntry] items are converted
  /// to labelled buttons; [LayrzDropdownLabel] items are silently skipped.
  /// An empty list renders nothing in both modes.
  final List<LayrzDropdownItem> items;

  /// Forces the render mode. When null, the mode follows the responsive breakpoint,
  /// collapsing to the dropdown below `md`.
  ///
  /// - `true`: always render dropdown mode
  /// - `false`: always render row mode
  /// - `null` (default): switch automatically at the md breakpoint
  final bool? useDropdown;

  /// Gap between buttons in row mode. Defaults to `tokens.spacing.sp2` (8 logical pixels).
  ///
  /// Only applies in row mode. Unused in dropdown mode.
  final double? spacing;

  /// Icon shown on the collapsed trigger. Defaults to the overflow-dots icon.
  ///
  /// Only applies in dropdown mode.
  final IconData? triggerIcon;

  /// Accessible name and tooltip for the collapsed trigger button.
  ///
  /// Shown as the trigger's tooltip on hover and announced by screen readers.
  /// Platform overflow menus render a stable control name rather than enumerating
  /// their contents, so this is required unconditionally — even though [useDropdown]
  /// defaults to null and the group may collapse at any viewport width, the caller
  /// always knows the semantic name to assign.
  final String triggerHintText;

  /// Horizontal alignment of the dropdown panel against the trigger.
  ///
  /// Defaults to [LayrzDropdownMenuAlignment.start].
  /// Only applies in dropdown mode.
  final LayrzDropdownMenuAlignment alignment;

  /// Creates a new [LayrzButtonGroup].
  ///
  /// The [items] and [triggerHintText] parameters are required. All others
  /// are optional with sensible defaults.

Builder Constructor

/// Creates a [LayrzButtonGroup] with a caller-supplied trigger widget.
///
/// The [builder] receives the menu [MenuController] and must wire it to the
/// trigger's own tap handler. This allows the caller to supply any trigger
/// widget and style it freely.
///
/// In row mode, the builder is never called and the group renders its items
/// as usual.
const LayrzButtonGroup.builder({
  required List<LayrzDropdownItem> items,
  required LayrzDropdownMenuBuilder builder,
  bool? useDropdown,
  double? spacing,
  LayrzDropdownMenuAlignment alignment = LayrzDropdownMenuAlignment.start,
});

Usage Examples

Row Mode (Forced)

LayrzButtonGroup(
  triggerHintText: 'Table actions',
  items: [
    LayrzDropdownEntry.save(labelText: 'Save', onTap: () { }),
    LayrzDropdownEntry.edit(labelText: 'Edit', onTap: () { }),
    LayrzDropdownEntry.delete(labelText: 'Delete', onTap: () { }),
  ],
  useDropdown: false,
)

Renders three labelled buttons in a horizontal row.

Dropdown Mode (Forced)

LayrzButtonGroup(
  triggerHintText: 'Table actions',
  items: [
    LayrzDropdownLabel(labelText: 'Modify'),
    LayrzDropdownEntry.save(labelText: 'Save', onTap: () { }),
    LayrzDropdownEntry.edit(labelText: 'Edit', onTap: () { }),
    LayrzDropdownLabel(labelText: 'Danger Zone'),
    LayrzDropdownEntry.delete(labelText: 'Delete', onTap: () { }),
  ],
  useDropdown: true,
)

Renders a single trigger button. When tapped, opens a menu showing all entries and labels. Labels organize entries into sections and render only in dropdown mode.

Responsive (Automatic)

LayrzButtonGroup(
  triggerHintText: 'Actions',
  items: [
    LayrzDropdownEntry.save(labelText: 'Save', onTap: () { }),
    LayrzDropdownEntry.delete(labelText: 'Delete', onTap: () { }),
  ],
  // useDropdown: null (default) — switches automatically at md breakpoint
)

At or above 960px, renders two labelled buttons. Below 960px, collapses to a single trigger opening the dropdown menu.

Custom Trigger

LayrzButtonGroup.builder(
  items: [
    LayrzDropdownEntry.save(labelText: 'Save', onTap: () { }),
    LayrzDropdownEntry.delete(labelText: 'Delete', onTap: () { }),
  ],
  useDropdown: true,
  builder: (context, controller) => LayrzButton(
    labelText: 'Options',
    icon: MdiIcons.cogOutline,
    style: LayrzButtonStyle.outlinedFab,
    onTap: controller.isOpen ? controller.close : controller.open,
  ),
)

Uses a custom button as the trigger, allowing full control over its styling.


Row Mode Entry-to-Button Conversion

In row mode, each LayrzDropdownEntry is converted to a LayrzButton with these rules:

Entry Property Button Property
labelText labelText
icon icon
onTap onTap (nulled if disabled)
enabled isDisabled (inverted)
color or semantic type color (with type: custom), resolved via entry.resolveAccent(tokens)
shortcut (dropped — LayrzButton has no shortcut field)

The button style is always LayrzButtonStyle.elevated (a labelled, non-Fab button).

Semantic colour mapping: When an entry is created via a semantic factory (.save(), .cancel(), .info(), .show(), .edit(), .delete()), the semantic type's colour token (success, danger, info, warning) is automatically resolved and passed to the button. If an explicit color is supplied to a semantic factory, it takes precedence over the semantic type. This ensures that the button's accent colour matches the entry's visual intent, whether the entry comes from a semantic factory or carries a custom colour.

Label skipping: LayrzDropdownLabel items are completely omitted in row mode. They serve only to organize entries in the dropdown menu.


Related Widgets

Clone this wiki locally