// Copyright 2021 The Flutter team. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; const rowDivider = SizedBox(width: 20); const colDivider = SizedBox(height: 10); const tinySpacing = 3.0; const smallSpacing = 10.0; const double cardWidth = 115; const double widthConstraint = 450; class FirstComponentList extends StatelessWidget { const FirstComponentList({ super.key, required this.showNavBottomBar, required this.scaffoldKey, required this.showSecondList, }); final bool showNavBottomBar; final GlobalKey scaffoldKey; final bool showSecondList; @override Widget build(BuildContext context) { List children = [ const Actions(), colDivider, const Communication(), colDivider, const Containment(), if (!showSecondList) ...[ colDivider, Navigation(scaffoldKey: scaffoldKey), colDivider, const Selection(), colDivider, const TextInputs() ], ]; List heights = List.filled(children.length, null); // Fully traverse this list before moving on. return FocusTraversalGroup( child: CustomScrollView( slivers: [ SliverPadding( padding: showSecondList ? const EdgeInsetsDirectional.only(end: smallSpacing) : EdgeInsets.zero, sliver: SliverList( delegate: BuildSlivers( heights: heights, builder: (context, index) { return _CacheHeight( heights: heights, index: index, child: children[index], ); }, ), ), ), ], ), ); } } class SecondComponentList extends StatelessWidget { const SecondComponentList({ super.key, required this.scaffoldKey, }); final GlobalKey scaffoldKey; @override Widget build(BuildContext context) { List children = [ Navigation(scaffoldKey: scaffoldKey), colDivider, const Selection(), colDivider, const TextInputs(), ]; List heights = List.filled(children.length, null); // Fully traverse this list before moving on. return FocusTraversalGroup( child: CustomScrollView( slivers: [ SliverPadding( padding: const EdgeInsetsDirectional.only(end: smallSpacing), sliver: SliverList( delegate: BuildSlivers( heights: heights, builder: (context, index) { return _CacheHeight( heights: heights, index: index, child: children[index], ); }, ), ), ), ], ), ); } } // If the content of a CustomScrollView does not change, then it's // safe to cache the heights of each item as they are laid out. The // sum of the cached heights are returned by an override of // `SliverChildDelegate.estimateMaxScrollOffset`. The default version // of this method bases its estimate on the average height of the // visible items. The override ensures that the scrollbar thumb's // size, which depends on the max scroll offset, will shrink smoothly // as the contents of the list are exposed for the first time, and // then remain fixed. class _CacheHeight extends SingleChildRenderObjectWidget { const _CacheHeight({ super.child, required this.heights, required this.index, }); final List heights; final int index; @override RenderObject createRenderObject(BuildContext context) { return _RenderCacheHeight( heights: heights, index: index, ); } @override void updateRenderObject( BuildContext context, _RenderCacheHeight renderObject) { renderObject ..heights = heights ..index = index; } } class _RenderCacheHeight extends RenderProxyBox { _RenderCacheHeight({ required List heights, required int index, }) : _heights = heights, _index = index, super(); List _heights; List get heights => _heights; set heights(List value) { if (value == _heights) { return; } _heights = value; markNeedsLayout(); } int _index; int get index => _index; set index(int value) { if (value == index) { return; } _index = value; markNeedsLayout(); } @override void performLayout() { super.performLayout(); heights[index] = size.height; } } // The heights information is used to override the `estimateMaxScrollOffset` and // provide a more accurate estimation for the max scroll offset. class BuildSlivers extends SliverChildBuilderDelegate { BuildSlivers({ required NullableIndexedWidgetBuilder builder, required this.heights, }) : super(builder, childCount: heights.length); final List heights; @override double? estimateMaxScrollOffset(int firstIndex, int lastIndex, double leadingScrollOffset, double trailingScrollOffset) { return heights.reduce((sum, height) => (sum ?? 0) + (height ?? 0))!; } } class Actions extends StatelessWidget { const Actions({super.key}); @override Widget build(BuildContext context) { return const ComponentGroupDecoration(label: 'Actions', children: [ Buttons(), FloatingActionButtons(), IconToggleButtons(), SegmentedButtons(), ]); } } class Communication extends StatelessWidget { const Communication({super.key}); @override Widget build(BuildContext context) { return const ComponentGroupDecoration(label: 'Communication', children: [ NavigationBars( selectedIndex: 1, isExampleBar: true, isBadgeExample: true, ), ProgressIndicators(), SnackBarSection(), ]); } } class Containment extends StatelessWidget { const Containment({super.key}); @override Widget build(BuildContext context) { return const ComponentGroupDecoration(label: 'Containment', children: [ BottomSheetSection(), Cards(), Dialogs(), Dividers(), // TODO: Add Lists, https://github.com/flutter/flutter/issues/114006 // TODO: Add Side sheets, https://github.com/flutter/flutter/issues/119328 ]); } } class Navigation extends StatelessWidget { const Navigation({super.key, required this.scaffoldKey}); final GlobalKey scaffoldKey; @override Widget build(BuildContext context) { return ComponentGroupDecoration(label: 'Navigation', children: [ const BottomAppBars(), const NavigationBars( selectedIndex: 0, isExampleBar: true, ), NavigationDrawers(scaffoldKey: scaffoldKey), const NavigationRails(), const Tabs(), const SearchAnchors(), const TopAppBars(), ]); } } class Selection extends StatelessWidget { const Selection({super.key}); @override Widget build(BuildContext context) { return const ComponentGroupDecoration(label: 'Selection', children: [ Checkboxes(), Chips(), DatePicker(), TimePicker(), Menus(), Radios(), Sliders(), Switches(), ]); } } class TextInputs extends StatelessWidget { const TextInputs({super.key}); @override Widget build(BuildContext context) { return const ComponentGroupDecoration( label: 'Text inputs', children: [TextFields()], ); } } class Buttons extends StatefulWidget { const Buttons({super.key}); @override State createState() => _ButtonsState(); } class _ButtonsState extends State { @override Widget build(BuildContext context) { return const ComponentDecoration( label: 'Common buttons', tooltipMessage: 'Use ElevatedButton, FilledButton, FilledButton.tonal, OutlinedButton, or TextButton', child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ ButtonsWithoutIcon(isDisabled: false), ButtonsWithIcon(), ButtonsWithoutIcon(isDisabled: true), ], ), ), ); } } class ButtonsWithoutIcon extends StatelessWidget { final bool isDisabled; const ButtonsWithoutIcon({super.key, required this.isDisabled}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 5.0), child: IntrinsicWidth( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ElevatedButton( onPressed: isDisabled ? null : () {}, child: const Text('Elevated'), ), colDivider, FilledButton( onPressed: isDisabled ? null : () {}, child: const Text('Filled'), ), colDivider, FilledButton.tonal( onPressed: isDisabled ? null : () {}, child: const Text('Filled tonal'), ), colDivider, OutlinedButton( onPressed: isDisabled ? null : () {}, child: const Text('Outlined'), ), colDivider, TextButton( onPressed: isDisabled ? null : () {}, child: const Text('Text'), ), ], ), ), ); } } class ButtonsWithIcon extends StatelessWidget { const ButtonsWithIcon({super.key}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 10.0), child: IntrinsicWidth( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ElevatedButton.icon( onPressed: () {}, icon: const Icon(Icons.add), label: const Text('Icon'), ), colDivider, FilledButton.icon( onPressed: () {}, label: const Text('Icon'), icon: const Icon(Icons.add), ), colDivider, FilledButton.tonalIcon( onPressed: () {}, label: const Text('Icon'), icon: const Icon(Icons.add), ), colDivider, OutlinedButton.icon( onPressed: () {}, icon: const Icon(Icons.add), label: const Text('Icon'), ), colDivider, TextButton.icon( onPressed: () {}, icon: const Icon(Icons.add), label: const Text('Icon'), ) ], ), ), ); } } class FloatingActionButtons extends StatelessWidget { const FloatingActionButtons({super.key}); @override Widget build(BuildContext context) { return ComponentDecoration( label: 'Floating action buttons', tooltipMessage: 'Use FloatingActionButton or FloatingActionButton.extended', child: Wrap( crossAxisAlignment: WrapCrossAlignment.center, runSpacing: smallSpacing, spacing: smallSpacing, children: [ FloatingActionButton.small( onPressed: () {}, tooltip: 'Small', child: const Icon(Icons.add), ), FloatingActionButton.extended( onPressed: () {}, tooltip: 'Extended', icon: const Icon(Icons.add), label: const Text('Create'), ), FloatingActionButton( onPressed: () {}, tooltip: 'Standard', child: const Icon(Icons.add), ), FloatingActionButton.large( onPressed: () {}, tooltip: 'Large', child: const Icon(Icons.add), ), ], ), ); } } class Cards extends StatelessWidget { const Cards({super.key}); @override Widget build(BuildContext context) { return ComponentDecoration( label: 'Cards', tooltipMessage: 'Use Card', child: Wrap( alignment: WrapAlignment.spaceEvenly, children: [ SizedBox( width: cardWidth, child: Card( child: Container( padding: const EdgeInsets.fromLTRB(10, 5, 5, 10), child: Column( children: [ Align( alignment: Alignment.topRight, child: IconButton( icon: const Icon(Icons.more_vert), onPressed: () {}, ), ), const SizedBox(height: 20), const Align( alignment: Alignment.bottomLeft, child: Text('Elevated'), ) ], ), ), ), ), SizedBox( width: cardWidth, child: Card( color: Theme.of(context).colorScheme.surfaceContainerHighest, elevation: 0, child: Container( padding: const EdgeInsets.fromLTRB(10, 5, 5, 10), child: Column( children: [ Align( alignment: Alignment.topRight, child: IconButton( icon: const Icon(Icons.more_vert), onPressed: () {}, ), ), const SizedBox(height: 20), const Align( alignment: Alignment.bottomLeft, child: Text('Filled'), ) ], ), ), ), ), SizedBox( width: cardWidth, child: Card( elevation: 0, shape: RoundedRectangleBorder( side: BorderSide( color: Theme.of(context).colorScheme.outline, ), borderRadius: const BorderRadius.all(Radius.circular(12)), ), child: Container( padding: const EdgeInsets.fromLTRB(10, 5, 5, 10), child: Column( children: [ Align( alignment: Alignment.topRight, child: IconButton( icon: const Icon(Icons.more_vert), onPressed: () {}, ), ), const SizedBox(height: 20), const Align( alignment: Alignment.bottomLeft, child: Text('Outlined'), ) ], ), ), ), ), ], ), ); } } class _ClearButton extends StatelessWidget { const _ClearButton({required this.controller}); final TextEditingController controller; @override Widget build(BuildContext context) => IconButton( icon: const Icon(Icons.clear), onPressed: () => controller.clear(), ); } class TextFields extends StatefulWidget { const TextFields({super.key}); @override State createState() => _TextFieldsState(); } class _TextFieldsState extends State { final TextEditingController _controllerFilled = TextEditingController(); final TextEditingController _controllerOutlined = TextEditingController(); @override Widget build(BuildContext context) { return ComponentDecoration( label: 'Text fields', tooltipMessage: 'Use TextField with different InputDecoration', child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.all(smallSpacing), child: TextField( controller: _controllerFilled, decoration: InputDecoration( prefixIcon: const Icon(Icons.search), suffixIcon: _ClearButton(controller: _controllerFilled), labelText: 'Filled', hintText: 'hint text', helperText: 'supporting text', filled: true, ), ), ), Padding( padding: const EdgeInsets.all(smallSpacing), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: SizedBox( width: 200, child: TextField( maxLength: 10, maxLengthEnforcement: MaxLengthEnforcement.none, controller: _controllerFilled, decoration: InputDecoration( prefixIcon: const Icon(Icons.search), suffixIcon: _ClearButton(controller: _controllerFilled), labelText: 'Filled', hintText: 'hint text', helperText: 'supporting text', filled: true, errorText: 'error text', ), ), ), ), const SizedBox(width: smallSpacing), Flexible( child: SizedBox( width: 200, child: TextField( controller: _controllerFilled, enabled: false, decoration: InputDecoration( prefixIcon: const Icon(Icons.search), suffixIcon: _ClearButton(controller: _controllerFilled), labelText: 'Disabled', hintText: 'hint text', helperText: 'supporting text', filled: true, ), ), ), ), ], ), ), Padding( padding: const EdgeInsets.all(smallSpacing), child: TextField( controller: _controllerOutlined, decoration: InputDecoration( prefixIcon: const Icon(Icons.search), suffixIcon: _ClearButton(controller: _controllerOutlined), labelText: 'Outlined', hintText: 'hint text', helperText: 'supporting text', border: const OutlineInputBorder(), ), ), ), Padding( padding: const EdgeInsets.all(smallSpacing), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: SizedBox( width: 200, child: TextField( controller: _controllerOutlined, decoration: InputDecoration( prefixIcon: const Icon(Icons.search), suffixIcon: _ClearButton(controller: _controllerOutlined), labelText: 'Outlined', hintText: 'hint text', helperText: 'supporting text', errorText: 'error text', border: const OutlineInputBorder(), filled: true, ), ), ), ), const SizedBox(width: smallSpacing), Flexible( child: SizedBox( width: 200, child: TextField( controller: _controllerOutlined, enabled: false, decoration: InputDecoration( prefixIcon: const Icon(Icons.search), suffixIcon: _ClearButton(controller: _controllerOutlined), labelText: 'Disabled', hintText: 'hint text', helperText: 'supporting text', border: const OutlineInputBorder(), filled: true, ), ), ), ), ])), ], ), ); } } class Dialogs extends StatefulWidget { const Dialogs({super.key}); @override State createState() => _DialogsState(); } class _DialogsState extends State { void openDialog(BuildContext context) { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('What is a dialog?'), content: const Text( 'A dialog is a type of modal window that appears in front of app content to provide critical information, or prompt for a decision to be made.'), actions: [ TextButton( child: const Text('Dismiss'), onPressed: () => Navigator.of(context).pop(), ), FilledButton( child: const Text('Okay'), onPressed: () => Navigator.of(context).pop(), ), ], ), ); } void openFullscreenDialog(BuildContext context) { showDialog( context: context, builder: (context) => Dialog.fullscreen( child: Padding( padding: const EdgeInsets.all(20.0), child: Scaffold( appBar: AppBar( title: const Text('Full-screen dialog'), centerTitle: false, leading: IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop(), ), actions: [ TextButton( child: const Text('Close'), onPressed: () => Navigator.of(context).pop(), ), ], ), ), ), ), ); } @override Widget build(BuildContext context) { return ComponentDecoration( label: 'Dialog', tooltipMessage: 'Use showDialog with Dialog.fullscreen, AlertDialog, or SimpleDialog', child: Wrap( alignment: WrapAlignment.spaceBetween, children: [ TextButton( child: const Text( 'Show dialog', style: TextStyle(fontWeight: FontWeight.bold), ), onPressed: () => openDialog(context), ), TextButton( child: const Text( 'Show full-screen dialog', style: TextStyle(fontWeight: FontWeight.bold), ), onPressed: () => openFullscreenDialog(context), ), ], ), ); } } class Dividers extends StatelessWidget { const Dividers({super.key}); @override Widget build(BuildContext context) { return const ComponentDecoration( label: 'Dividers', tooltipMessage: 'Use Divider or VerticalDivider', child: Column( children: [ Divider(key: Key('divider')), ], ), ); } } class Switches extends StatelessWidget { const Switches({super.key}); @override Widget build(BuildContext context) { return const ComponentDecoration( label: 'Switches', tooltipMessage: 'Use SwitchListTile or Switch', child: Column( children: [ SwitchRow(isEnabled: true), SwitchRow(isEnabled: false), ], ), ); } } class SwitchRow extends StatefulWidget { const SwitchRow({super.key, required this.isEnabled}); final bool isEnabled; @override State createState() => _SwitchRowState(); } class _SwitchRowState extends State { bool value0 = false; bool value1 = true; final WidgetStateProperty thumbIcon = WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.selected)) { return const Icon(Icons.check); } return const Icon(Icons.close); }); @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ // TODO: use SwitchListTile when thumbIcon is available https://github.com/flutter/flutter/issues/118616 Switch( value: value0, onChanged: widget.isEnabled ? (value) { setState(() { value0 = value; }); } : null, ), Switch( thumbIcon: thumbIcon, value: value1, onChanged: widget.isEnabled ? (value) { setState(() { value1 = value; }); } : null, ), ], ); } } class Checkboxes extends StatefulWidget { const Checkboxes({super.key}); @override State createState() => _CheckboxesState(); } class _CheckboxesState extends State { bool? isChecked0 = true; bool? isChecked1; bool? isChecked2 = false; @override Widget build(BuildContext context) { return ComponentDecoration( label: 'Checkboxes', tooltipMessage: 'Use CheckboxListTile or Checkbox', child: Column( children: [ CheckboxListTile( tristate: true, value: isChecked0, title: const Text('Option 1'), onChanged: (value) { setState(() { isChecked0 = value; }); }, ), CheckboxListTile( tristate: true, value: isChecked1, title: const Text('Option 2'), onChanged: (value) { setState(() { isChecked1 = value; }); }, ), CheckboxListTile( tristate: true, value: isChecked2, title: const Text('Option 3'), // TODO: showcase error state https://github.com/flutter/flutter/issues/118616 onChanged: (value) { setState(() { isChecked2 = value; }); }, ), const CheckboxListTile( tristate: true, title: Text('Option 4'), value: true, onChanged: null, ), ], ), ); } } enum Value { first, second } class Radios extends StatefulWidget { const Radios({super.key}); @override State createState() => _RadiosState(); } enum Options { option1, option2, option3 } class _RadiosState extends State { Options? _selectedOption = Options.option1; @override Widget build(BuildContext context) { return ComponentDecoration( label: 'Radio buttons', tooltipMessage: 'Use RadioListTile or Radio', child: Column( children: [ RadioListTile( title: const Text('Option 1'), value: Options.option1, groupValue: _selectedOption, onChanged: (value) { setState(() { _selectedOption = value; }); }, ), RadioListTile( title: const Text('Option 2'), value: Options.option2, groupValue: _selectedOption, onChanged: (value) { setState(() { _selectedOption = value; }); }, ), RadioListTile( title: const Text('Option 3'), value: Options.option3, groupValue: _selectedOption, onChanged: null, ), ], ), ); } } class ProgressIndicators extends StatefulWidget { const ProgressIndicators({super.key}); @override State createState() => _ProgressIndicatorsState(); } class _ProgressIndicatorsState extends State { bool playProgressIndicator = false; @override Widget build(BuildContext context) { final double? progressValue = playProgressIndicator ? null : 0.7; return ComponentDecoration( label: 'Progress indicators', tooltipMessage: 'Use CircularProgressIndicator or LinearProgressIndicator', child: Column( children: [ Row( children: [ IconButton( isSelected: playProgressIndicator, selectedIcon: const Icon(Icons.pause), icon: const Icon(Icons.play_arrow), onPressed: () { setState(() { playProgressIndicator = !playProgressIndicator; }); }, ), Expanded( child: Row( children: [ rowDivider, CircularProgressIndicator( value: progressValue, ), rowDivider, Expanded( child: LinearProgressIndicator( value: progressValue, ), ), rowDivider, ], ), ), ], ), ], ), ); } } const List appBarDestinations = [ NavigationDestination( tooltip: '', icon: Icon(Icons.widgets_outlined), label: 'Components', selectedIcon: Icon(Icons.widgets), ), NavigationDestination( tooltip: '', icon: Icon(Icons.format_paint_outlined), label: 'Color', selectedIcon: Icon(Icons.format_paint), ), NavigationDestination( tooltip: '', icon: Icon(Icons.text_snippet_outlined), label: 'Typography', selectedIcon: Icon(Icons.text_snippet), ), NavigationDestination( tooltip: '', icon: Icon(Icons.invert_colors_on_outlined), label: 'Elevation', selectedIcon: Icon(Icons.opacity), ) ]; const List exampleBarDestinations = [ NavigationDestination( tooltip: '', icon: Icon(Icons.explore_outlined), label: 'Explore', selectedIcon: Icon(Icons.explore), ), NavigationDestination( tooltip: '', icon: Icon(Icons.pets_outlined), label: 'Pets', selectedIcon: Icon(Icons.pets), ), NavigationDestination( tooltip: '', icon: Icon(Icons.account_box_outlined), label: 'Account', selectedIcon: Icon(Icons.account_box), ) ]; List barWithBadgeDestinations = [ NavigationDestination( tooltip: '', icon: Badge.count(count: 1000, child: const Icon(Icons.mail_outlined)), label: 'Mail', selectedIcon: Badge.count(count: 1000, child: const Icon(Icons.mail)), ), const NavigationDestination( tooltip: '', icon: Badge(label: Text('10'), child: Icon(Icons.chat_bubble_outline)), label: 'Chat', selectedIcon: Badge(label: Text('10'), child: Icon(Icons.chat_bubble)), ), const NavigationDestination( tooltip: '', icon: Badge(child: Icon(Icons.group_outlined)), label: 'Rooms', selectedIcon: Badge(child: Icon(Icons.group_rounded)), ), NavigationDestination( tooltip: '', icon: Badge.count(count: 3, child: const Icon(Icons.videocam_outlined)), label: 'Meet', selectedIcon: Badge.count(count: 3, child: const Icon(Icons.videocam)), ) ]; class NavigationBars extends StatefulWidget { const NavigationBars({ super.key, this.onSelectItem, required this.selectedIndex, required this.isExampleBar, this.isBadgeExample = false, }); final void Function(int)? onSelectItem; final int selectedIndex; final bool isExampleBar; final bool isBadgeExample; @override State createState() => _NavigationBarsState(); } class _NavigationBarsState extends State { late int selectedIndex; @override void initState() { super.initState(); selectedIndex = widget.selectedIndex; } @override void didUpdateWidget(covariant NavigationBars oldWidget) { super.didUpdateWidget(oldWidget); if (widget.selectedIndex != oldWidget.selectedIndex) { selectedIndex = widget.selectedIndex; } } @override Widget build(BuildContext context) { // App NavigationBar should get first focus. Widget navigationBar = Focus( autofocus: !(widget.isExampleBar || widget.isBadgeExample), child: NavigationBar( selectedIndex: selectedIndex, onDestinationSelected: (index) { setState(() { selectedIndex = index; }); if (!widget.isExampleBar) widget.onSelectItem!(index); }, destinations: widget.isExampleBar && widget.isBadgeExample ? barWithBadgeDestinations : widget.isExampleBar ? exampleBarDestinations : appBarDestinations, ), ); if (widget.isExampleBar && widget.isBadgeExample) { navigationBar = ComponentDecoration( label: 'Badges', tooltipMessage: 'Use Badge or Badge.count', child: navigationBar); } else if (widget.isExampleBar) { navigationBar = ComponentDecoration( label: 'Navigation bar', tooltipMessage: 'Use NavigationBar', child: navigationBar); } return navigationBar; } } class IconToggleButtons extends StatefulWidget { const IconToggleButtons({super.key}); @override State createState() => _IconToggleButtonsState(); } class _IconToggleButtonsState extends State { bool standardSelected = false; bool filledSelected = false; bool tonalSelected = false; bool outlinedSelected = false; @override Widget build(BuildContext context) { return ComponentDecoration( label: 'Icon buttons', tooltipMessage: 'Use IconButton, IconButton.filled, IconButton.filledTonal, and IconButton.outlined', child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Column( // Standard IconButton children: [ IconButton( isSelected: standardSelected, icon: const Icon(Icons.settings_outlined), selectedIcon: const Icon(Icons.settings), onPressed: () { setState(() { standardSelected = !standardSelected; }); }, ), colDivider, IconButton( isSelected: standardSelected, icon: const Icon(Icons.settings_outlined), selectedIcon: const Icon(Icons.settings), onPressed: null, ), ], ), Column( children: [ // Filled IconButton IconButton.filled( isSelected: filledSelected, icon: const Icon(Icons.settings_outlined), selectedIcon: const Icon(Icons.settings), onPressed: () { setState(() { filledSelected = !filledSelected; }); }, ), colDivider, IconButton.filled( isSelected: filledSelected, icon: const Icon(Icons.settings_outlined), selectedIcon: const Icon(Icons.settings), onPressed: null, ), ], ), Column( children: [ // Filled Tonal IconButton IconButton.filledTonal( isSelected: tonalSelected, icon: const Icon(Icons.settings_outlined), selectedIcon: const Icon(Icons.settings), onPressed: () { setState(() { tonalSelected = !tonalSelected; }); }, ), colDivider, IconButton.filledTonal( isSelected: tonalSelected, icon: const Icon(Icons.settings_outlined), selectedIcon: const Icon(Icons.settings), onPressed: null, ), ], ), Column( children: [ // Outlined IconButton IconButton.outlined( isSelected: outlinedSelected, icon: const Icon(Icons.settings_outlined), selectedIcon: const Icon(Icons.settings), onPressed: () { setState(() { outlinedSelected = !outlinedSelected; }); }, ), colDivider, IconButton.outlined( isSelected: outlinedSelected, icon: const Icon(Icons.settings_outlined), selectedIcon: const Icon(Icons.settings), onPressed: null, ), ], ), ], ), ); } } class Chips extends StatefulWidget { const Chips({super.key}); @override State createState() => _ChipsState(); } class _ChipsState extends State { bool isFiltered = true; @override Widget build(BuildContext context) { return ComponentDecoration( label: 'Chips', tooltipMessage: 'Use ActionChip, FilterChip, or InputChip. \nActionChip can also be used for suggestion chip', child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Wrap( spacing: smallSpacing, runSpacing: smallSpacing, children: [ ActionChip( label: const Text('Assist'), avatar: const Icon(Icons.event), onPressed: () {}, ), FilterChip( label: const Text('Filter'), selected: isFiltered, onSelected: (selected) { setState(() => isFiltered = selected); }, ), InputChip( label: const Text('Input'), onPressed: () {}, onDeleted: () {}, ), ActionChip( label: const Text('Suggestion'), onPressed: () {}, ), ], ), colDivider, Wrap( spacing: smallSpacing, runSpacing: smallSpacing, children: [ const ActionChip( label: Text('Assist'), avatar: Icon(Icons.event), ), FilterChip( label: const Text('Filter'), selected: isFiltered, onSelected: null, ), InputChip( label: const Text('Input'), onDeleted: () {}, isEnabled: false, ), const ActionChip( label: Text('Suggestion'), ), ], ), ], ), ); } } class DatePicker extends StatefulWidget { const DatePicker({super.key}); @override State createState() => _DatePickerState(); } class _DatePickerState extends State { DateTime? selectedDate; final DateTime _firstDate = DateTime(DateTime.now().year - 2); final DateTime _lastDate = DateTime(DateTime.now().year + 1); @override Widget build(BuildContext context) { return ComponentDecoration( label: 'Date picker', tooltipMessage: 'Use showDatePicker', child: TextButton.icon( onPressed: () async { DateTime? date = await showDatePicker( context: context, initialDate: selectedDate ?? DateTime.now(), firstDate: _firstDate, lastDate: _lastDate, ); setState(() { selectedDate = date; if (selectedDate != null) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text( 'Selected Date: ${selectedDate!.day}/${selectedDate!.month}/${selectedDate!.year}'), )); } }); }, icon: const Icon(Icons.calendar_month), label: const Text( 'Show date picker', style: TextStyle(fontWeight: FontWeight.bold), ), ), ); } } class TimePicker extends StatefulWidget { const TimePicker({super.key}); @override State createState() => _TimePickerState(); } class _TimePickerState extends State { TimeOfDay? selectedTime; @override Widget build(BuildContext context) { return ComponentDecoration( label: 'Time picker', tooltipMessage: 'Use showTimePicker', child: TextButton.icon( onPressed: () async { final TimeOfDay? time = await showTimePicker( context: context, initialTime: selectedTime ?? TimeOfDay.now(), builder: (context, child) { return MediaQuery( data: MediaQuery.of(context).copyWith( alwaysUse24HourFormat: true, ), child: child!, ); }, ); setState(() { selectedTime = time; if (selectedTime != null) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text('Selected time: ${selectedTime!.format(context)}'), )); } }); }, icon: const Icon(Icons.schedule), label: const Text( 'Show time picker', style: TextStyle(fontWeight: FontWeight.bold), ), ), ); } } class SegmentedButtons extends StatelessWidget { const SegmentedButtons({super.key}); @override Widget build(BuildContext context) { return const ComponentDecoration( label: 'Segmented buttons', tooltipMessage: 'Use SegmentedButton', child: Column( children: [ SingleChoice(), colDivider, MultipleChoice(), ], ), ); } } enum Calendar { day, week, month, year } class SingleChoice extends StatefulWidget { const SingleChoice({super.key}); @override State createState() => _SingleChoiceState(); } class _SingleChoiceState extends State { Calendar calendarView = Calendar.day; @override Widget build(BuildContext context) { return SegmentedButton( segments: const >[ ButtonSegment( value: Calendar.day, label: Text('Day'), icon: Icon(Icons.calendar_view_day)), ButtonSegment( value: Calendar.week, label: Text('Week'), icon: Icon(Icons.calendar_view_week)), ButtonSegment( value: Calendar.month, label: Text('Month'), icon: Icon(Icons.calendar_view_month)), ButtonSegment( value: Calendar.year, label: Text('Year'), icon: Icon(Icons.calendar_today)), ], selected: {calendarView}, onSelectionChanged: (newSelection) { setState(() { // By default there is only a single segment that can be // selected at one time, so its value is always the first // item in the selected set. calendarView = newSelection.first; }); }, ); } } enum Sizes { extraSmall, small, medium, large, extraLarge } class MultipleChoice extends StatefulWidget { const MultipleChoice({super.key}); @override State createState() => _MultipleChoiceState(); } class _MultipleChoiceState extends State { Set selection = {Sizes.large, Sizes.extraLarge}; @override Widget build(BuildContext context) { return SegmentedButton( segments: const >[ ButtonSegment(value: Sizes.extraSmall, label: Text('XS')), ButtonSegment(value: Sizes.small, label: Text('S')), ButtonSegment(value: Sizes.medium, label: Text('M')), ButtonSegment( value: Sizes.large, label: Text('L'), ), ButtonSegment(value: Sizes.extraLarge, label: Text('XL')), ], selected: selection, onSelectionChanged: (newSelection) { setState(() { selection = newSelection; }); }, multiSelectionEnabled: true, ); } } class SnackBarSection extends StatelessWidget { const SnackBarSection({super.key}); @override Widget build(BuildContext context) { return ComponentDecoration( label: 'Snackbar', tooltipMessage: 'Use ScaffoldMessenger.of(context).showSnackBar with SnackBar', child: TextButton( onPressed: () { final snackBar = SnackBar( behavior: SnackBarBehavior.floating, width: 400.0, content: const Text('This is a snackbar'), action: SnackBarAction( label: 'Close', onPressed: () {}, ), ); ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar(snackBar); }, child: const Text( 'Show snackbar', style: TextStyle(fontWeight: FontWeight.bold), ), ), ); } } class BottomSheetSection extends StatefulWidget { const BottomSheetSection({super.key}); @override State createState() => _BottomSheetSectionState(); } class _BottomSheetSectionState extends State { bool isNonModalBottomSheetOpen = false; PersistentBottomSheetController? _nonModalBottomSheetController; @override Widget build(BuildContext context) { List buttonList = [ IconButton(onPressed: () {}, icon: const Icon(Icons.share_outlined)), IconButton(onPressed: () {}, icon: const Icon(Icons.add)), IconButton(onPressed: () {}, icon: const Icon(Icons.delete_outline)), IconButton(onPressed: () {}, icon: const Icon(Icons.archive_outlined)), IconButton(onPressed: () {}, icon: const Icon(Icons.settings_outlined)), IconButton(onPressed: () {}, icon: const Icon(Icons.favorite_border)), ]; List labelList = const [ Text('Share'), Text('Add to'), Text('Trash'), Text('Archive'), Text('Settings'), Text('Favorite') ]; buttonList = List.generate( buttonList.length, (index) => Padding( padding: const EdgeInsets.fromLTRB(20.0, 30.0, 20.0, 20.0), child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ buttonList[index], labelList[index], ], ), )); return ComponentDecoration( label: 'Bottom sheet', tooltipMessage: 'Use showModalBottomSheet or showBottomSheet', child: Wrap( alignment: WrapAlignment.spaceEvenly, children: [ TextButton( child: const Text( 'Show modal bottom sheet', style: TextStyle(fontWeight: FontWeight.bold), ), onPressed: () { showModalBottomSheet( showDragHandle: true, context: context, // TODO: Remove when this is in the framework https://github.com/flutter/flutter/issues/118619 constraints: const BoxConstraints(maxWidth: 640), builder: (context) { return SizedBox( height: 150, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 32.0), child: ListView( shrinkWrap: true, scrollDirection: Axis.horizontal, children: buttonList, ), ), ); }, ); }, ), TextButton( child: Text( isNonModalBottomSheetOpen ? 'Hide bottom sheet' : 'Show bottom sheet', style: const TextStyle(fontWeight: FontWeight.bold), ), onPressed: () { if (isNonModalBottomSheetOpen) { _nonModalBottomSheetController?.close(); setState(() { isNonModalBottomSheetOpen = false; }); return; } else { setState(() { isNonModalBottomSheetOpen = true; }); } _nonModalBottomSheetController = showBottomSheet( elevation: 8.0, context: context, // TODO: Remove when this is in the framework https://github.com/flutter/flutter/issues/118619 constraints: const BoxConstraints(maxWidth: 640), builder: (context) { return SizedBox( height: 150, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 32.0), child: ListView( shrinkWrap: true, scrollDirection: Axis.horizontal, children: buttonList, ), ), ); }, ); }, ), ], ), ); } } class BottomAppBars extends StatelessWidget { const BottomAppBars({super.key}); @override Widget build(BuildContext context) { return ComponentDecoration( label: 'Bottom app bar', tooltipMessage: 'Use BottomAppBar', child: Column( children: [ SizedBox( height: 80, child: Scaffold( floatingActionButton: FloatingActionButton( onPressed: () {}, elevation: 0.0, child: const Icon(Icons.add), ), floatingActionButtonLocation: FloatingActionButtonLocation.endContained, bottomNavigationBar: BottomAppBar( child: Row( children: [ const IconButtonAnchorExample(), IconButton( tooltip: 'Search', icon: const Icon(Icons.search), onPressed: () {}, ), IconButton( tooltip: 'Favorite', icon: const Icon(Icons.favorite), onPressed: () {}, ), ], ), ), ), ), ], ), ); } } class IconButtonAnchorExample extends StatelessWidget { const IconButtonAnchorExample({super.key}); @override Widget build(BuildContext context) { return MenuAnchor( builder: (context, controller, child) { return IconButton( onPressed: () { if (controller.isOpen) { controller.close(); } else { controller.open(); } }, icon: const Icon(Icons.more_vert), ); }, menuChildren: [ MenuItemButton( child: const Text('Menu 1'), onPressed: () {}, ), MenuItemButton( child: const Text('Menu 2'), onPressed: () {}, ), SubmenuButton( menuChildren: [ MenuItemButton( onPressed: () {}, child: const Text('Menu 3.1'), ), MenuItemButton( onPressed: () {}, child: const Text('Menu 3.2'), ), MenuItemButton( onPressed: () {}, child: const Text('Menu 3.3'), ), ], child: const Text('Menu 3'), ), ], ); } } class ButtonAnchorExample extends StatelessWidget { const ButtonAnchorExample({super.key}); @override Widget build(BuildContext context) { return MenuAnchor( builder: (context, controller, child) { return FilledButton.tonal( onPressed: () { if (controller.isOpen) { controller.close(); } else { controller.open(); } }, child: const Text('Show menu'), ); }, menuChildren: [ MenuItemButton( leadingIcon: const Icon(Icons.people_alt_outlined), child: const Text('Item 1'), onPressed: () {}, ), MenuItemButton( leadingIcon: const Icon(Icons.remove_red_eye_outlined), child: const Text('Item 2'), onPressed: () {}, ), MenuItemButton( leadingIcon: const Icon(Icons.refresh), onPressed: () {}, child: const Text('Item 3'), ), ], ); } } class NavigationDrawers extends StatelessWidget { const NavigationDrawers({super.key, required this.scaffoldKey}); final GlobalKey scaffoldKey; @override Widget build(BuildContext context) { return ComponentDecoration( label: 'Navigation drawer', tooltipMessage: 'Use NavigationDrawer. For modal navigation drawers, see Scaffold.endDrawer', child: Column( children: [ const SizedBox(height: 520, child: NavigationDrawerSection()), colDivider, colDivider, TextButton( child: const Text('Show modal navigation drawer', style: TextStyle(fontWeight: FontWeight.bold)), onPressed: () { scaffoldKey.currentState!.openEndDrawer(); }, ), ], ), ); } } class NavigationDrawerSection extends StatefulWidget { const NavigationDrawerSection({super.key}); @override State createState() => _NavigationDrawerSectionState(); } class _NavigationDrawerSectionState extends State { int navDrawerIndex = 0; @override Widget build(BuildContext context) { return NavigationDrawer( onDestinationSelected: (selectedIndex) { setState(() { navDrawerIndex = selectedIndex; }); }, selectedIndex: navDrawerIndex, children: [ Padding( padding: const EdgeInsets.fromLTRB(28, 16, 16, 10), child: Text( 'Mail', style: Theme.of(context).textTheme.titleSmall, ), ), ...destinations.map((destination) { return NavigationDrawerDestination( label: Text(destination.label), icon: destination.icon, selectedIcon: destination.selectedIcon, ); }), const Divider(indent: 28, endIndent: 28), Padding( padding: const EdgeInsets.fromLTRB(28, 16, 16, 10), child: Text( 'Labels', style: Theme.of(context).textTheme.titleSmall, ), ), ...labelDestinations.map((destination) { return NavigationDrawerDestination( label: Text(destination.label), icon: destination.icon, selectedIcon: destination.selectedIcon, ); }), ], ); } } class ExampleDestination { const ExampleDestination(this.label, this.icon, this.selectedIcon); final String label; final Widget icon; final Widget selectedIcon; } const List destinations = [ ExampleDestination('Inbox', Icon(Icons.inbox_outlined), Icon(Icons.inbox)), ExampleDestination('Outbox', Icon(Icons.send_outlined), Icon(Icons.send)), ExampleDestination( 'Favorites', Icon(Icons.favorite_outline), Icon(Icons.favorite)), ExampleDestination('Trash', Icon(Icons.delete_outline), Icon(Icons.delete)), ]; const List labelDestinations = [ ExampleDestination( 'Family', Icon(Icons.bookmark_border), Icon(Icons.bookmark)), ExampleDestination( 'School', Icon(Icons.bookmark_border), Icon(Icons.bookmark)), ExampleDestination('Work', Icon(Icons.bookmark_border), Icon(Icons.bookmark)), ]; class NavigationRails extends StatelessWidget { const NavigationRails({super.key}); @override Widget build(BuildContext context) { return const ComponentDecoration( label: 'Navigation rail', tooltipMessage: 'Use NavigationRail', child: IntrinsicWidth( child: SizedBox(height: 420, child: NavigationRailSection())), ); } } class NavigationRailSection extends StatefulWidget { const NavigationRailSection({super.key}); @override State createState() => _NavigationRailSectionState(); } class _NavigationRailSectionState extends State { int navRailIndex = 0; @override Widget build(BuildContext context) { return NavigationRail( onDestinationSelected: (selectedIndex) { setState(() { navRailIndex = selectedIndex; }); }, elevation: 4, leading: FloatingActionButton( child: const Icon(Icons.create), onPressed: () {}), groupAlignment: 0.0, selectedIndex: navRailIndex, labelType: NavigationRailLabelType.selected, destinations: [ ...destinations.map((destination) { return NavigationRailDestination( label: Text(destination.label), icon: destination.icon, selectedIcon: destination.selectedIcon, ); }), ], ); } } class Tabs extends StatefulWidget { const Tabs({super.key}); @override State createState() => _TabsState(); } class _TabsState extends State with TickerProviderStateMixin { late TabController _tabController; @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); } @override Widget build(BuildContext context) { return ComponentDecoration( label: 'Tabs', tooltipMessage: 'Use TabBar', child: SizedBox( height: 80, child: Scaffold( appBar: AppBar( bottom: TabBar( controller: _tabController, tabs: const [ Tab( icon: Icon(Icons.videocam_outlined), text: 'Video', iconMargin: EdgeInsets.only(bottom: 0.0), ), Tab( icon: Icon(Icons.photo_outlined), text: 'Photos', iconMargin: EdgeInsets.only(bottom: 0.0), ), Tab( icon: Icon(Icons.audiotrack_sharp), text: 'Audio', iconMargin: EdgeInsets.only(bottom: 0.0), ), ], ), // TODO: Showcase secondary tab bar https://github.com/flutter/flutter/issues/111962 ), ), ), ); } } class TopAppBars extends StatelessWidget { const TopAppBars({super.key}); static final actions = [ IconButton(icon: const Icon(Icons.attach_file), onPressed: () {}), IconButton(icon: const Icon(Icons.event), onPressed: () {}), IconButton(icon: const Icon(Icons.more_vert), onPressed: () {}), ]; @override Widget build(BuildContext context) { return ComponentDecoration( label: 'Top app bars', tooltipMessage: 'Use AppBar, SliverAppBar, SliverAppBar.medium, or SliverAppBar.large', child: Column( children: [ AppBar( title: const Text('Center-aligned'), leading: const BackButton(), actions: [ IconButton( iconSize: 32, icon: const Icon(Icons.account_circle_outlined), onPressed: () {}, ), ], centerTitle: true, ), colDivider, AppBar( title: const Text('Small'), leading: const BackButton(), actions: actions, centerTitle: false, ), colDivider, SizedBox( height: 100, child: CustomScrollView( slivers: [ SliverAppBar.medium( title: const Text('Medium'), leading: const BackButton(), actions: actions, ), const SliverFillRemaining(), ], ), ), colDivider, SizedBox( height: 130, child: CustomScrollView( slivers: [ SliverAppBar.large( title: const Text('Large'), leading: const BackButton(), actions: actions, ), const SliverFillRemaining(), ], ), ), ], ), ); } } class Menus extends StatefulWidget { const Menus({super.key}); @override State createState() => _MenusState(); } class _MenusState extends State { final TextEditingController colorController = TextEditingController(); final TextEditingController iconController = TextEditingController(); IconLabel? selectedIcon = IconLabel.smile; ColorLabel? selectedColor; @override Widget build(BuildContext context) { final List> colorEntries = >[]; for (final ColorLabel color in ColorLabel.values) { colorEntries.add(DropdownMenuEntry( value: color, label: color.label, enabled: color.label != 'Grey')); } final List> iconEntries = >[]; for (final IconLabel icon in IconLabel.values) { iconEntries .add(DropdownMenuEntry(value: icon, label: icon.label)); } return ComponentDecoration( label: 'Menus', tooltipMessage: 'Use MenuAnchor or DropdownMenu', child: Column( children: [ const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ButtonAnchorExample(), rowDivider, IconButtonAnchorExample(), ], ), colDivider, Wrap( alignment: WrapAlignment.spaceAround, runAlignment: WrapAlignment.center, crossAxisAlignment: WrapCrossAlignment.center, spacing: smallSpacing, runSpacing: smallSpacing, children: [ DropdownMenu( controller: colorController, label: const Text('Color'), enableFilter: true, dropdownMenuEntries: colorEntries, inputDecorationTheme: const InputDecorationTheme(filled: true), onSelected: (color) { setState(() { selectedColor = color; }); }, ), DropdownMenu( initialSelection: IconLabel.smile, controller: iconController, leadingIcon: const Icon(Icons.search), label: const Text('Icon'), dropdownMenuEntries: iconEntries, onSelected: (icon) { setState(() { selectedIcon = icon; }); }, ), Icon( selectedIcon?.icon, color: selectedColor?.color ?? Colors.grey.withOpacity(0.5), ) ], ), ], ), ); } } enum ColorLabel { blue('Blue', Colors.blue), pink('Pink', Colors.pink), green('Green', Colors.green), yellow('Yellow', Colors.yellow), grey('Grey', Colors.grey); const ColorLabel(this.label, this.color); final String label; final Color color; } enum IconLabel { smile('Smile', Icons.sentiment_satisfied_outlined), cloud( 'Cloud', Icons.cloud_outlined, ), brush('Brush', Icons.brush_outlined), heart('Heart', Icons.favorite); const IconLabel(this.label, this.icon); final String label; final IconData icon; } class Sliders extends StatefulWidget { const Sliders({super.key}); @override State createState() => _SlidersState(); } class _SlidersState extends State { double sliderValue0 = 30.0; double sliderValue1 = 20.0; @override Widget build(BuildContext context) { return ComponentDecoration( label: 'Sliders', tooltipMessage: 'Use Slider or RangeSlider', child: Column( children: [ Slider( max: 100, value: sliderValue0, onChanged: (value) { setState(() { sliderValue0 = value; }); }, ), const SizedBox(height: 20), Slider( max: 100, divisions: 5, value: sliderValue1, label: sliderValue1.round().toString(), onChanged: (value) { setState(() { sliderValue1 = value; }); }, ), ], )); } } class SearchAnchors extends StatefulWidget { const SearchAnchors({super.key}); @override State createState() => _SearchAnchorsState(); } class _SearchAnchorsState extends State { String? selectedColor; List searchHistory = []; Iterable getHistoryList(SearchController controller) { return searchHistory.map((color) => ListTile( leading: const Icon(Icons.history), title: Text(color.label), trailing: IconButton( icon: const Icon(Icons.call_missed), onPressed: () { controller.text = color.label; controller.selection = TextSelection.collapsed(offset: controller.text.length); }), onTap: () { controller.closeView(color.label); handleSelection(color); }, )); } Iterable getSuggestions(SearchController controller) { final String input = controller.value.text; return ColorItem.values .where((color) => color.label.contains(input)) .map((filteredColor) => ListTile( leading: CircleAvatar(backgroundColor: filteredColor.color), title: Text(filteredColor.label), trailing: IconButton( icon: const Icon(Icons.call_missed), onPressed: () { controller.text = filteredColor.label; controller.selection = TextSelection.collapsed(offset: controller.text.length); }), onTap: () { controller.closeView(filteredColor.label); handleSelection(filteredColor); }, )); } void handleSelection(ColorItem color) { setState(() { selectedColor = color.label; if (searchHistory.length >= 5) { searchHistory.removeLast(); } searchHistory.insert(0, color); }); } @override Widget build(BuildContext context) { return ComponentDecoration( label: 'Search', tooltipMessage: 'Use SearchAnchor or SearchAnchor.bar', child: Column( children: [ SearchAnchor.bar( barHintText: 'Search colors', suggestionsBuilder: (context, controller) { if (controller.text.isEmpty) { if (searchHistory.isNotEmpty) { return getHistoryList(controller); } return [ const Center( child: Text('No search history.', style: TextStyle(color: Colors.grey)), ) ]; } return getSuggestions(controller); }, ), const SizedBox(height: 20), if (selectedColor == null) const Text('Select a color') else Text('Last selected color is $selectedColor') ], ), ); } } class ComponentDecoration extends StatefulWidget { const ComponentDecoration({ super.key, required this.label, required this.child, this.tooltipMessage = '', }); final String label; final Widget child; final String? tooltipMessage; @override State createState() => _ComponentDecorationState(); } class _ComponentDecorationState extends State { final focusNode = FocusNode(); @override Widget build(BuildContext context) { return RepaintBoundary( child: Padding( padding: const EdgeInsets.symmetric(vertical: smallSpacing), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(widget.label, style: Theme.of(context).textTheme.titleSmall), Tooltip( message: widget.tooltipMessage, child: const Padding( padding: EdgeInsets.symmetric(horizontal: 5.0), child: Icon(Icons.info_outline, size: 16)), ), ], ), ConstrainedBox( constraints: const BoxConstraints.tightFor(width: widthConstraint), // Tapping within the a component card should request focus // for that component's children. child: Focus( focusNode: focusNode, canRequestFocus: true, child: GestureDetector( onTapDown: (_) { focusNode.requestFocus(); }, behavior: HitTestBehavior.opaque, child: Card( elevation: 0, shape: RoundedRectangleBorder( side: BorderSide( color: Theme.of(context).colorScheme.outlineVariant, ), borderRadius: const BorderRadius.all(Radius.circular(12)), ), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 5.0, vertical: 20.0), child: Center( child: widget.child, ), ), ), ), ), ), ], ), ), ); } } class ComponentGroupDecoration extends StatelessWidget { const ComponentGroupDecoration( {super.key, required this.label, required this.children}); final String label; final List children; @override Widget build(BuildContext context) { // Fully traverse this component group before moving on return FocusTraversalGroup( child: Card( margin: EdgeInsets.zero, elevation: 0, color: Theme.of(context) .colorScheme .surfaceContainerHighest .withOpacity(0.3), child: Padding( padding: const EdgeInsets.symmetric(vertical: 20.0), child: Center( child: Column( children: [ Text(label, style: Theme.of(context).textTheme.titleLarge), colDivider, ...children ], ), ), ), ), ); } } enum ColorItem { red('red', Colors.red), orange('orange', Colors.orange), yellow('yellow', Colors.yellow), green('green', Colors.green), blue('blue', Colors.blue), indigo('indigo', Colors.indigo), violet('violet', Color(0xFF8F00FF)), purple('purple', Colors.purple), pink('pink', Colors.pink), silver('silver', Color(0xFF808080)), gold('gold', Color(0xFFFFD700)), beige('beige', Color(0xFFF5F5DC)), brown('brown', Colors.brown), grey('grey', Colors.grey), black('black', Colors.black), white('white', Colors.white); const ColorItem(this.label, this.color); final String label; final Color color; }